先存一波

master
LAPTOP-S9HJSOEB\昊天 5 months ago
parent ddd467b8cf
commit 98ec4d7821

@ -140,5 +140,6 @@
"package.json": "pnpm-lock.yaml,yarn.lock,LICENSE,README*,CHANGELOG*,CNAME,.gitattributes,.eslintrc-auto-import.json,.gitignore,prettier.config.js,stylelint.config.js,commitlint.config.js,.stylelintignore,.prettierignore,.gitpod.yml,.eslintrc.js,.eslintignore"
},
"terminal.integrated.scrollback": 10000,
"nuxt.isNuxtApp": false
"nuxt.isNuxtApp": false,
"codingcopilot.autoRun": true
}

18487
package-lock.json generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 977 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

@ -1,46 +1,59 @@
import request from '@/config/axios'
// 数据集管理 VO
export interface DatasVO {
id: number // id
name: string // 项目名称
description: string // 描述
status: string // 类型,还未标注,正在标注,正在训练,训练完成
path: string // 路径
count: number // 图片总数
type: string // 识别类型
progress: number // 进度
}
// 数据集管理 API
export const DatasApi = {
// 查询数据集管理分页
getDatasPage: async (params: any) => {
return await request.get({ url: `/annotation/datas/page`, params })
},
// 查询数据集管理详情
getDatas: async (id: number) => {
return await request.get({ url: `/annotation/datas/get?id=` + id })
},
// 新增数据集管理
createDatas: async (data: DatasVO) => {
return await request.post({ url: `/annotation/datas/create`, data })
},
// 修改数据集管理
updateDatas: async (data: DatasVO) => {
return await request.put({ url: `/annotation/datas/update`, data })
},
// 删除数据集管理
deleteDatas: async (id: number) => {
return await request.delete({ url: `/annotation/datas/delete?id=` + id })
},
// 导出数据集管理 Excel
exportDatas: async (params) => {
return await request.download({ url: `/annotation/datas/export-excel`, params })
},
}
import request from '@/config/axios'
// 数据集管理 VO
export interface DatasVO {
id: number // id
name: string // 项目名称
description: string // 描述
status: string // 类型,还未标注,正在标注,正在训练,训练完成
path: string // 路径
count: number // 图片总数
type: string // 识别类型
progress: number // 进度
}
// 数据集管理 API
export const DatasApi = {
// 查询数据集管理分页
getDatasPage: async (params: any) => {
return await request.get({ url: `/annotation/datas/page`, params })
},
refreshDatas: async (id: number) => {
return await request.get({ url: `/annotation/datas/refreshDatas?id=` + id })
},
// 查询数据集管理详情
getDatas: async (id: number) => {
return await request.get({ url: `/annotation/datas/get?id=` + id })
},
// 新增数据集管理
createDatas: async (data: DatasVO) => {
return await request.post({ url: `/annotation/datas/create`, data })
},
// 修改数据集管理
updateDatas: async (data: DatasVO) => {
return await request.put({ url: `/annotation/datas/update`, data })
},
// 在 DatasApi 中添加增强方法(示例)
enhanceImages(data: { id: number; enhancements: string[] }): Promise<void> {
return request.post({
url: `/annotation/datas/enhance`,
data: {
id:data.id,
enhancements: data.enhancements
}
})
},
// 删除数据集管理
deleteDatas: async (id: number) => {
return await request.delete({ url: `/annotation/datas/delete?id=` + id })
},
// 导出数据集管理 Excel
exportDatas: async (params) => {
return await request.download({ url: `/annotation/datas/export-excel`, params })
},
}

@ -13,6 +13,7 @@ export interface DataVO {
createTime: string
path: string
count: number
status: string
type: string
progress: number
}
@ -92,6 +93,11 @@ export const MarkApi = {
getProjectList: async () => {
return await request.post({ url: `/annotation/datas/list` })
},
// 获取标注类型列表
getProjectStatusList: async (data: { status: number }) => {
return await request.post({ url: `/annotation/datas/listStatus`,data })
},
// 创建标注类型
createType: async (data: Partial<AnnotationTypeVO>) => {
return await request.post({ url: `/annotation/types/create`, data })

@ -1,49 +1,79 @@
import request from '@/config/axios'
// 训练 VO
export interface TrainVO {
id: number // id
dataId: number // 项目id
train: number // 训练集比例
val: number // 验证图像比例
test: number // 测试图像比例
round: number // 轮次
size: number // 批次大小
imageSize: number // 图片大小(正方向,大于这个值进行缩放,小于这个值进行放大,不是正方形将图片周围涂黑)
modelPath: string // 预选训练模型
path: string // 训练图片路径
trainType: number // 类型使用哪个gpu
}
// 训练 API
export const TrainApi = {
// 查询训练分页
getTrainPage: async (params: any) => {
return await request.get({ url: `/annotation/train/page`, params })
},
// 查询训练详情
getTrain: async (id: number) => {
return await request.get({ url: `/annotation/train/get?id=` + id })
},
// 新增训练
createTrain: async (data: TrainVO) => {
return await request.post({ url: `/annotation/train/create`, data })
},
// 修改训练
updateTrain: async (data: TrainVO) => {
return await request.put({ url: `/annotation/train/update`, data })
},
// 删除训练
deleteTrain: async (id: number) => {
return await request.delete({ url: `/annotation/train/delete?id=` + id })
},
// 导出训练 Excel
exportTrain: async (params) => {
return await request.download({ url: `/annotation/train/export-excel`, params })
},
}
import request from '@/config/axios'
// 训练 VO
export interface TrainVO {
id: number // id
name: string // 训练名称
dataId: number // 项目id
train: number // 训练集比例
val: number // 验证图像比例
test: number // 测试图像比例
round: number // 轮次
size: number // 批次大小
imageSize: number // 图片大小(正方向,大于这个值进行缩放,小于这个值进行放大,不是正方形将图片周围涂黑)
modelPath: string // 预选训练模型
path: string // 训练图片路径
trainType: number // 类型使用哪个gpu
visualType?: string // 识别类型(从项目获取)
}
// 训练 API
export const TrainApi = {
// 查询训练分页
getTrainPage: async (params: any) => {
return await request.get({ url: `/annotation/train/page`, params })
},
// 查询训练详情
getTrain: async (id: number) => {
return await request.get({ url: `/annotation/train/get?id=` + id })
},
// 新增训练
createTrain: async (data: TrainVO) => {
return await request.post({ url: `/annotation/train/create`, data })
},
// 修改训练
updateTrain: async (data: TrainVO) => {
return await request.put({ url: `/annotation/train/update`, data })
},
// 初始化
InfinityTrain: async (data: TrainVO) => {
return await request.put({ url: `/annotation/train/init`, data })
},
// 查找环境
environmentInquiry: async () => {
return await request.post({ url: `/annotation/yolo/detect-env` })
},
// 删除训练
deleteTrain: async (id: number) => {
return await request.delete({ url: `/annotation/train/delete?id=` + id })
},
// 导出训练 Excel
exportTrain: async (params) => {
return await request.download({ url: `/annotation/train/export-excel`, params })
},
// 获取训练结果
getTrainStatus: async (trainId: number) => {
return await request.get({ url: `/annotation/train-result/status?trainId=` + trainId })
},
// 获取训练状态
getTrainInfoStatus: async (trainId: number) => {
return await request.get({ url: `/annotation/train-Info/get-list?trainId=` + trainId })
},
// 开始训练
handleTrain: async (data: TrainVO) => {
return await request.post({ url: `/annotation/yolo/train?trainId=` + data.id })
},
// 导出训练数据
exportTrainData: async (trainId: number) => {
return await request.download({ url: `/annotation/train/export-data?trainId=` + trainId })
},
}

@ -7,8 +7,8 @@
label-width="100px"
v-loading="formLoading"
>
<el-form-item label="项目名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入项目名称" />
<el-form-item label="数据集名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入数据集名称" />
</el-form-item>
<el-form-item label="描述" prop="description">
<el-input v-model="formData.description" placeholder="描述" />
@ -59,7 +59,7 @@ const formData = ref({
status: undefined,
path: undefined,
count: undefined,
type: undefined,
type: undefined as number | undefined,
progress: undefined,
})
const formRules = reactive({
@ -77,6 +77,8 @@ const open = async (type: string, id?: number) => {
formLoading.value = true
try {
formData.value = await DatasApi.getDatas(id)
// type
formData.value.type = formData.value.type ? Number(formData.value.type) : undefined
} finally {
formLoading.value = false
}

@ -8,10 +8,10 @@
:inline="true"
label-width="68px"
>
<el-form-item label="项目名称" prop="name">
<el-form-item label="数据集" class="" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入项目名称"
placeholder="请输入数据集名称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
@ -86,7 +86,7 @@
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<el-table-column label="项目名称" align="center" prop="name" />
<el-table-column label="数据集名称" align="center" prop="name" />
<el-table-column label="描述" align="center" prop="description" />
<!-- 还未标注正在标注正在训练训练完成 -->
<el-table-column label="类型" align="center" prop="status" >
@ -114,64 +114,80 @@
:formatter="dateFormatter"
width="180px"
/>
<el-table-column
width="400px" label="操作" align="center">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['annotation:datas:update']"
>
编辑
</el-button>
<el-button
link
type="primary"
@click="handleDelete(scope.row.id)"
v-hasPermi="['annotation:datas:delete']"
>
刷新
</el-button>
<el-button
link
type="primary"
@click="handleDelete(scope.row.id)"
v-hasPermi="['annotation:datas:delete']"
>
增强
</el-button>
<router-link :to="'/dict/type/data/' + scope.row.id">
<el-button link type="primary">标注</el-button>
</router-link>
<router-link :to="'/dict/type/data/' + scope.row.id">
<el-button link type="primary">训练</el-button>
</router-link>
<router-link :to="'/dict/type/data/' + scope.row.id">
<el-button link type="primary">测试</el-button>
</router-link>
<el-button
link
type="primary"
@click="handleDelete(scope.row.id)"
v-hasPermi="['annotation:datas:delete']"
>
导出
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['annotation:datas:delete']"
>
删除
</el-button>
</template>
</el-table-column>
<!-- 修改后的完整操作列 -->
<el-table-column width="400px" label="操作" align="center">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
>
编辑
</el-button>
<el-button
link
type="primary"
@click="handleRefresh(scope.row.id)"
>
刷新
</el-button>
<el-button
link
type="primary"
@click="handleEnhance(scope.row)"
>
增强
</el-button>
<router-link :to="'/annotation/mark?id=' + scope.row.id ">
<el-button
link
type="primary"
:disabled="scope.row.status < 1"
>
标注
</el-button>
</router-link>
<router-link :to="'/annotation/train?name=' + encodeURIComponent(scope.row.name)">
<el-button
link
type="primary"
:disabled="scope.row.status < 2"
>
训练
</el-button>
</router-link>
<router-link
v-if="scope.row.status === 4"
:to="'/annotation/test/' + scope.row.id"
>
<el-button link type="primary">测试</el-button>
</router-link>
<el-button
v-if="scope.row.status === 4"
link
type="primary"
@click="handleExportModel(scope.row.id)"
v-hasPermi="['annotation:datas:export']"
>
导出
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['annotation:datas:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
@ -184,6 +200,29 @@
<!-- 表单弹窗添加/修改 -->
<DatasForm ref="formRef" @success="getList" />
<!-- 图像增强对话框 -->
<el-dialog
v-model="enhanceDialogVisible"
title="图像增强"
width="500px"
@close="selectedEnhancements = []"
>
<el-checkbox-group v-model="selectedEnhancements" class="enhancement-checkbox-group">
<el-checkbox
v-for="option in enhancementOptions"
:key="option.value"
:label="option.value"
class="enhancement-checkbox"
>
{{ option.label }}
</el-checkbox>
</el-checkbox-group>
<template #footer>
<el-button @click="enhanceDialogVisible = false">取消</el-button>
<el-button type="primary" @click="executeEnhancement"></el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
@ -217,7 +256,44 @@ const queryParams = reactive({
})
const queryFormRef = ref() //
const exportLoading = ref(false) //
//
const enhanceDialogVisible = ref(false)
const selectedEnhancements = ref<string[]>([])
const currentEnhanceRow = ref<DatasVO | null>(null)
//
const enhancementOptions = [
{ label: '亮度调整Brightness随机调整图像的亮度', value: 'brightness' },
{ label: '对比度调整Contrast增强或减少图像的对比度', value: 'contrast' },
{ label: '饱和度调整Saturation改变图像的色彩饱和度', value: 'saturation' },
{ label: '色相调整Hue随机改变图像的色相改变颜色的基调', value: 'hue' },
{ label: '灰度化Grayscale将图像转换为灰度图模拟不同的拍摄条件', value: 'grayscale' }
]
//
const executeEnhancement = async () => {
if (!currentEnhanceRow.value || selectedEnhancements.value.length === 0) {
message.warning('请选择至少一种增强方式')
return
}
try {
// API
await DatasApi.enhanceImages({
id: currentEnhanceRow.value.id,
enhancements: selectedEnhancements.value
})
message.success('图像增强任务已启动')
enhanceDialogVisible.value = false
//
await getList()
} catch (error) {
message.error('增强操作失败: ' + (error as Error).message)
}
}
/** 查询列表 */
const getList = async () => {
loading.value = true
@ -229,7 +305,57 @@ const getList = async () => {
loading.value = false
}
}
//
const handleRefresh = async (id: number) => {
try {
await DatasApi.refreshDatas(id)
message.success('刷新成功')
await getList()
} catch (error) {
message.error('刷新失败: ' + (error as Error).message)
}
}
//
const handleEnhance = async (row: DatasVO) => {
//
// await handleRefresh(row.id)
await DatasApi.refreshDatas(row.id)
//
currentEnhanceRow.value = row
enhanceDialogVisible.value = true
//
// await getList()
const updatedRow = list.value.find(item => item.id === row.id)
// (2)
if (updatedRow && updatedRow.status !== "2") {
message.warning('请先完成标注进度达到100%后再进行增强操作')
return
}
console.log(updatedRow);
selectedEnhancements.value = []
}
//
const handleExportModel = async (id: number) => {
try {
//
await message.exportConfirm()
//
const data = await DatasApi.exportModel(id)
//
download.excel(data, '模型导出.zip')
message.success('导出成功')
} catch (error) {
message.error('导出失败: ' + (error as Error).message)
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
@ -280,4 +406,22 @@ const handleExport = async () => {
onMounted(() => {
getList()
})
</script>
</script>
<style scoped>
.enhancement-checkbox-group {
display: flex;
flex-direction: column;
gap: 15px;
}
.enhancement-checkbox {
white-space: normal;
line-height: 1.5;
padding: 10px;
border-radius: 4px;
&:hover {
background-color: #f5f7fa;
}
}
</style>

File diff suppressed because it is too large Load Diff

@ -4,40 +4,55 @@
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
label-width="120px"
v-loading="formLoading"
>
<el-form-item label="项目id" prop="dataId">
<el-input v-model="formData.dataId" placeholder="请输入项目id" />
<el-form-item label="训练名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入训练名称" />
</el-form-item>
<el-form-item label="训练集比例" prop="train">
<el-input v-model="formData.train" placeholder="请输入训练集比例" />
</el-form-item>
<el-form-item label="验证图像比例" prop="val">
<el-input v-model="formData.val" placeholder="请输入验证图像比例" />
<el-form-item label="数据集" prop="dataId">
<el-select v-model="formData.dataId" placeholder="请选择数据集" style="width: 100%">
<el-option
v-for="project in projectList"
:key="project.id"
:label="project.name"
:value="project.id"
/>
</el-select>
</el-form-item>
<el-form-item label="测试图像比例" prop="test">
<el-input v-model="formData.test" placeholder="请输入测试图像比例" />
<el-form-item label="数据集分配" prop="ratios">
<div class="ratio-slider-container">
<div class="ratio-labels">
<span>训练集: {{ formData.train }}%</span>
<span>验证集: {{ formData.val }}%</span>
<span>测试集: {{ formData.test }}%</span>
</div>
<el-slider
v-model="ratioRange"
range
:min="0"
:max="100"
@change="handleRatioChange"
:marks="marks"
:format-tooltip="formatTooltip"
/>
</div>
</el-form-item>
<el-form-item label="轮次" prop="round">
<el-input v-model="formData.round" placeholder="请输入轮次" />
<el-input-number v-model="formData.round" :min="1" :max="1000" placeholder="请输入轮次" />
</el-form-item>
<el-form-item label="批次大小" prop="size">
<el-input v-model="formData.size" placeholder="请输入批次大小" />
</el-form-item>
<el-form-item label="图片大小(正方向,大于这个值进行缩放,小于这个值进行放大,不是正方形将图片周围涂黑)" prop="imageSize">
<el-input v-model="formData.imageSize" placeholder="请输入图片大小(正方向,大于这个值进行缩放,小于这个值进行放大,不是正方形将图片周围涂黑)" />
</el-form-item>
<el-form-item label="预选训练模型" prop="modelPath">
<el-input v-model="formData.modelPath" placeholder="请输入预选训练模型" />
<el-input-number v-model="formData.size" :min="1" :max="128" placeholder="请输入批次大小" />
</el-form-item>
<el-form-item label="训练图片路径" prop="path">
<el-input v-model="formData.path" placeholder="请输入训练图片路径" />
</el-form-item>
<el-form-item label="类型使用哪个gpu" prop="trainType">
<el-select v-model="formData.trainType" placeholder="请选择类型使用哪个gpu">
<el-option label="请选择字典生成" value="" />
</el-select>
<el-form-item label="图片大小" prop="imageSize">
<el-input-number
v-model="formData.imageSize"
:min="32"
:max="16384"
:step="32"
placeholder="请输入图片大小"
/>
<div class="form-tip">正方形尺寸非正方形图片将自动填充黑色背景必须为32的倍数</div>
</el-form-item>
</el-form>
<template #footer>
@ -48,6 +63,7 @@
</template>
<script setup lang="ts">
import { TrainApi, TrainVO } from '@/api/annotation/train'
import { MarkApi, DataVO } from '@/api/annotation/mark'
/** 训练 表单 */
defineOptions({ name: 'TrainForm' })
@ -59,37 +75,116 @@ const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const projectList = ref<DataVO[]>([]) //
const formData = ref({
id: undefined,
name: undefined,
dataId: undefined,
train: undefined,
val: undefined,
test: undefined,
round: undefined,
size: undefined,
imageSize: undefined,
modelPath: undefined,
path: undefined,
trainType: undefined,
train: 70,
val: 20,
test: 10,
round: 4,
size: 50,
imageSize: 640,
})
// [, +]
const ratioRange = ref([70, 90])
//
const marks = {
0: '0%',
25: '25%',
50: '50%',
75: '75%',
100: '100%'
}
const formRules = reactive({
name: [{ required: true, message: '训练名称不能为空', trigger: 'blur' }],
dataId: [{ required: true, message: '数据集不能为空', trigger: 'change' }],
round: [{ required: true, message: '轮次不能为空', trigger: 'blur' }],
size: [{ required: true, message: '批次大小不能为空', trigger: 'blur' }],
imageSize: [
{ required: true, message: '图片大小不能为空', trigger: 'blur' },
{
validator: (rule: any, value: number, callback: Function) => {
if (value && value % 32 !== 0) {
callback(new Error('图片大小必须是32的倍数'))
} else {
callback()
}
},
trigger: 'blur'
}
],
})
const formRef = ref() // Ref
//
const getProjectList = async () => {
try {
const response = await MarkApi.getProjectList()
projectList.value = response
} catch (error) {
console.error('获取项目列表失败:', error)
}
}
//
const handleRatioChange = (value: [number, number]) => {
const [trainEnd, valEnd] = value
formData.value.train = trainEnd
formData.value.val = valEnd - trainEnd
formData.value.test = 100 - valEnd
// 100
formData.value.train = Math.round(formData.value.train)
formData.value.val = Math.round(formData.value.val)
formData.value.test = Math.round(formData.value.test)
//
ratioRange.value = [formData.value.train, formData.value.train + formData.value.val]
}
//
const formatTooltip = (value: number) => {
const [trainEnd, valEnd] = ratioRange.value
if (value === trainEnd) {
return `训练集: ${formData.value.train}%`
} else if (value === valEnd) {
return `验证集结束: ${valEnd}%`
}
return `${value}%`
}
//
const initRatioRange = () => {
ratioRange.value = [formData.value.train, formData.value.train + formData.value.val]
}
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
//
await getProjectList()
//
if (id) {
formLoading.value = true
try {
formData.value = await TrainApi.getTrain(id)
initRatioRange()
} finally {
formLoading.value = false
}
} else {
initRatioRange()
}
}
defineExpose({ open }) // open
@ -122,17 +217,43 @@ const submitForm = async () => {
const resetForm = () => {
formData.value = {
id: undefined,
name: undefined,
dataId: undefined,
train: undefined,
val: undefined,
test: undefined,
round: undefined,
size: undefined,
imageSize: undefined,
modelPath: undefined,
path: undefined,
trainType: undefined,
train: 70,
val: 20,
test: 10,
round: 4,
size: 50,
imageSize: 640,
}
initRatioRange()
formRef.value?.resetFields()
}
</script>
</script>
<style scoped>
.ratio-slider-container {
display: flex;
flex-direction: column;
gap: 16px;
}
.ratio-labels {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 8px;
}
.ratio-labels span {
font-size: 14px;
font-weight: 500;
color: #606266;
}
.form-tip {
font-size: 12px;
color: #909399;
margin-top: 4px;
}
</style>

@ -6,110 +6,32 @@
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
label-width="80px"
>
<el-form-item label="项目id" prop="dataId">
<el-form-item label="训练名称" prop="name">
<el-input
v-model="queryParams.dataId"
placeholder="请输入项目id"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="训练集比例" prop="train">
<el-input
v-model="queryParams.train"
placeholder="请输入训练集比例"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="验证图像比例" prop="val">
<el-input
v-model="queryParams.val"
placeholder="请输入验证图像比例"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="测试图像比例" prop="test">
<el-input
v-model="queryParams.test"
placeholder="请输入测试图像比例"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="轮次" prop="round">
<el-input
v-model="queryParams.round"
placeholder="请输入轮次"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="批次大小" prop="size">
<el-input
v-model="queryParams.size"
placeholder="请输入批次大小"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="图片大小(正方向,大于这个值进行缩放,小于这个值进行放大,不是正方形将图片周围涂黑)" prop="imageSize">
<el-input
v-model="queryParams.imageSize"
placeholder="请输入图片大小(正方向,大于这个值进行缩放,小于这个值进行放大,不是正方形将图片周围涂黑)"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="预选训练模型" prop="modelPath">
<el-input
v-model="queryParams.modelPath"
placeholder="请输入预选训练模型"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="训练图片路径" prop="path">
<el-input
v-model="queryParams.path"
placeholder="请输入训练图片路径"
v-model="queryParams.name"
placeholder="请输入训练名称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="类型使用哪个gpu" prop="trainType">
<el-form-item label="数据集" prop="dataId">
<el-select
v-model="queryParams.trainType"
placeholder="请选择类型使用哪个gpu"
v-model="queryParams.dataId"
placeholder="请选择数据集"
clearable
class="!w-240px"
>
<el-option label="请选择字典生成" value="" />
<el-option
v-for="project in projectList"
:key="project.id"
:label="project.name"
:value="project.id"
/>
</el-select>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
@ -128,7 +50,15 @@
:loading="exportLoading"
v-hasPermi="['annotation:train:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
<Icon icon="ep:search" class="mr-5px" /> 导出
</el-button>
<el-button
type="primary"
plain
@click="environmentInquiry()"
v-hasPermi="['annotation:train:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 环境查询
</el-button>
</el-form-item>
</el-form>
@ -137,25 +67,23 @@
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<el-table-column label="id" align="center" prop="id" />
<el-table-column label="项目id" align="center" prop="dataId" />
<el-table-column label="训练集比例" align="center" prop="train" />
<el-table-column label="验证图像比例" align="center" prop="val" />
<el-table-column label="测试图像比例" align="center" prop="test" />
<el-table-column label="轮次" align="center" prop="round" />
<el-table-column label="批次大小" align="center" prop="size" />
<el-table-column label="图片大小(正方向,大于这个值进行缩放,小于这个值进行放大,不是正方形将图片周围涂黑)" align="center" prop="imageSize" />
<el-table-column label="预选训练模型" align="center" prop="modelPath" />
<el-table-column label="训练图片路径" align="center" prop="path" />
<el-table-column label="类型使用哪个gpu" align="center" prop="trainType" />
<el-table-column
label="创建时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column label="操作" align="center">
<el-table-column label="训练名称" align="center" prop="name" />
<el-table-column label="数据集" align="center" prop="dataId">
<template #default="scope">
{{ getProjectName(scope.row.dataId) }}
</template>
</el-table-column>
<el-table-column label="训练类型" align="center" prop="trainType">
<template #default="scope">
<dict-tag :type="DICT_TYPE.TRAINING_STATUS" :value="scope.row.trainType" />
</template>
</el-table-column>
<el-table-column label="识别类型" align="center" prop="visualType">
<template #default="scope">
<dict-tag :type="DICT_TYPE.VISUAL_TYPE" :value="getProjectType(scope.row.dataId)" />
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="300">
<template #default="scope">
<el-button
link
@ -163,7 +91,29 @@
@click="openForm('update', scope.row.id)"
v-hasPermi="['annotation:train:update']"
>
编辑
修改
</el-button>
<el-button
link
type="success"
@click="initTrain(scope.row)"
>
初始化
</el-button>
<el-button
link
type="success"
@click="openTrainDrawer(scope.row)"
>
训练
</el-button>
<el-button
link
type="info"
@click="handleInfo(scope.row)"
>
信息
</el-button>
<el-button
link
@ -187,13 +137,111 @@
<!-- 表单弹窗添加/修改 -->
<TrainForm ref="formRef" @success="getList" />
<!-- 训练抽屉 -->
<el-drawer
v-model="trainDrawerVisible"
title="训练详情"
direction="rtl"
size="50%"
:before-close="closeTrainDrawer"
>
<div class="train-drawer-content">
<!-- 训练信息显示区域 -->
<div class="train-info">
<h3>当前训练: {{ currentTrain?.name }}</h3>
<div v-if="trainData" class="train-stats">
<div class="stat-item">
<span class="stat-label">训练状态:</span>
<el-tag :type="getStatusType(getTrainingStatus())">
{{ getTrainingStatus() }}
</el-tag>
</div>
<div class="stat-item" v-if="trainData.round !== undefined && trainData.roundTotal !== undefined">
<span class="stat-label">当前轮次:</span>
<span class="stat-value">{{ trainData.round }} / {{ trainData.roundTotal }}</span>
</div>
<div class="stat-item" v-if="trainData.round !== undefined && trainData.roundTotal !== undefined">
<span class="stat-label">训练进度:</span>
<el-progress
:percentage="trainProgress"
:status="trainData.round >= trainData.roundTotal ? 'success' : ''"
/>
</div>
<div class="stat-item" v-if="getMetricsValue('loss')">
<span class="stat-label">损失值:</span>
<span class="stat-value">{{ getMetricsValue('loss') }}</span>
</div>
<div class="stat-item" v-if="getMetricsValue('precision')">
<span class="stat-label">精度:</span>
<span class="stat-value">{{ (parseFloat(getMetricsValue('precision')) * 100).toFixed(2) + '%' }}</span>
</div>
<div class="stat-item" v-if="getMetricsValue('recall')">
<span class="stat-label">召回率:</span>
<span class="stat-value">{{ (parseFloat(getMetricsValue('recall')) * 100).toFixed(2) + '%' }}</span>
</div>
<div class="stat-item" v-if="getMetricsValue('map')">
<span class="stat-label">mAP:</span>
<span class="stat-value">{{ (parseFloat(getMetricsValue('map')) * 100).toFixed(2) + '%' }}</span>
</div>
<div class="stat-item" v-if="trainData.path">
<span class="stat-label">输出路径:</span>
<span class="stat-value">{{ trainData.path }}</span>
</div>
</div>
<div v-else class="loading-placeholder">
<el-skeleton :rows="5" animated />
</div>
</div>
<!-- 日志显示区域 -->
<div class="train-logs">
<h4>训练日志</h4>
<el-scrollbar height="300px" class="log-container">
<div v-if="trainLogs.length > 0" class="log-content">
<div v-for="(log, index) in trainLogs" :key="index" class="log-item">
<span class="log-time">{{ formatTime(log.timestamp) }}</span>
<span class="log-message">{{ log.message }}</span>
</div>
</div>
<div v-else class="empty-logs">
<el-empty description="暂无日志" />
</div>
</el-scrollbar>
</div>
</div>
<!-- 底部按钮区域 -->
<template #footer>
<div class="drawer-footer">
<el-button
type="primary"
@click="startTraining"
:disabled="isTraining"
:loading="isTraining"
>
{{ isTraining ? '训练中...' : '开始训练' }}
</el-button>
<el-button
@click="exportTrainData"
:disabled="!hasTrainResult"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
<el-button @click="closeTrainDrawer"></el-button>
</div>
</template>
</el-drawer>
</template>
<script setup lang="ts">
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import { TrainApi, TrainVO } from '@/api/annotation/train'
import { MarkApi, DataVO } from '@/api/annotation/mark'
import TrainForm from './TrainForm.vue'
import { DICT_TYPE } from '@/utils/dict'
/** 训练 列表 */
/** 训练 列表 */
defineOptions({ name: 'Train' })
@ -204,24 +252,55 @@ const { t } = useI18n() // 国际化
const loading = ref(true) //
const list = ref<TrainVO[]>([]) //
const total = ref(0) //
const projectList = ref<DataVO[]>([]) //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
name: undefined,
dataId: undefined,
train: undefined,
val: undefined,
test: undefined,
round: undefined,
size: undefined,
imageSize: undefined,
modelPath: undefined,
path: undefined,
trainType: undefined,
createTime: [],
})
const queryFormRef = ref() //
const exportLoading = ref(false) //
//
const trainDrawerVisible = ref(false)
const currentTrain = ref<TrainVO | null>(null)
const trainData = ref<any>(null)
const trainLogs = ref<Array<{timestamp: number, message: string}>>([])
const isTraining = ref(false)
const trainProgress = ref(0)
let trainTimer: NodeJS.Timeout | null = null
//
const getProjectList = async () => {
try {
// API
const response = await MarkApi.getProjectStatusList({status:2})
projectList.value = response
console.log(projectList.value)
} catch (error) {
console.error('获取项目列表失败:', error)
}
}
//
const getProjectName = (dataId: number) => {
const project = projectList.value.find(p => p.id === dataId)
return project ? project.name : '未知项目'
}
//
const getProjectStatus = (dataId: number) => {
const project = projectList.value.find(p => p.id === dataId)
return project ? project.status : ''
}
//
const getProjectType = (dataId: number) => {
const project = projectList.value.find(p => p.id === dataId)
return project ? project.type : ''
}
/** 查询列表 */
const getList = async () => {
loading.value = true
@ -252,6 +331,251 @@ const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 打开训练抽屉 */
const openTrainDrawer = (row: TrainVO) => {
currentTrain.value = row
trainDrawerVisible.value = true
startPolling()
}
/** 关闭训练抽屉 */
const closeTrainDrawer = () => {
trainDrawerVisible.value = false
stopPolling()
currentTrain.value = null
trainData.value = null
trainLogs.value = []
trainProgress.value = 0
isTraining.value = false
}
/** 开始轮询训练数据 */
const startPolling = () => {
//
fetchTrainData()
// 5
trainTimer = setInterval(() => {
fetchTrainData()
}, 5000)
}
/** 停止轮询 */
const stopPolling = () => {
if (trainTimer) {
clearInterval(trainTimer)
trainTimer = null
}
}
/** 获取训练数据 */
const fetchTrainData = async () => {
if (!currentTrain.value) return
try {
//
const [resultResponse, logResponse] = await Promise.all([
TrainApi.getTrainStatus(currentTrain.value.id),
TrainApi.getTrainInfoStatus(currentTrain.value.id)
])
//
if (Array.isArray(resultResponse) && resultResponse.length > 0) {
//
const latestRecord = resultResponse[0]
trainData.value = latestRecord
//
if (latestRecord.round === undefined && latestRecord.roundTotal === undefined) {
trainProgress.value = 0
} else {
//
const progress = Math.round((latestRecord.round / latestRecord.roundTotal) * 100)
trainProgress.value = Math.min(100, Math.max(0, progress))
}
} else {
//
trainData.value = {
trainId: currentTrain.value.id,
path: null,
rate: null,
dataId: null
}
trainProgress.value = 0
}
//
if (Array.isArray(logResponse) && logResponse.length > 0) {
//
logResponse.forEach(logItem => {
if (logItem && logItem.message) {
trainLogs.value.unshift({
timestamp: logItem.timestamp || Date.now(),
message: logItem.message
})
}
})
// 50
if (trainLogs.value.length > 50) {
trainLogs.value = trainLogs.value.slice(0, 50)
}
}
} catch (error) {
console.error('获取训练数据失败:', error)
trainData.value = {
trainId: currentTrain.value.id,
path: null,
rate: null,
dataId: null
}
trainProgress.value = 0
}
}
/** 获取训练状态 */
const getTrainingStatus = () => {
if (!trainData.value) return '未训练'
// trainId, path, rate, dataId
if (trainData.value.round === undefined && trainData.value.roundTotal === undefined) {
return '未训练'
}
if (trainData.value.round >= trainData.value.roundTotal) {
return '已完成'
}
return '训练中'
}
/** 从rate字符串中获取指标值 */
const getMetricsValue = (metric: string) => {
if (!trainData.value?.rate || trainData.value.rate === 'null') return null
const rateStr = trainData.value.rate
const metrics: Record<string, string> = {}
// "precision:0.7850,recall:0.7120,map:0.6450"
rateStr.split(',').forEach(item => {
const [key, value] = item.split(':')
if (key && value) {
metrics[key.trim().toLowerCase()] = value.trim()
}
})
// info
if (metric === 'loss' && trainData.value.info) {
const lossMatch = trainData.value.info.match(/损失值[:\s]+([\d.]+)/)
if (lossMatch) {
return lossMatch[1]
}
}
return metrics[metric] || null
}
/** 开始训练 */
const startTraining = async () => {
if (!currentTrain.value) return
try {
isTraining.value = true
await TrainApi.handleTrain(currentTrain.value)
message.success(`开始训练: ${currentTrain.value.name}`)
} catch (error) {
console.error('启动训练失败:', error)
message.error('启动训练失败')
} finally {
isTraining.value = false
}
}
/** 导出训练数据 */
const exportTrainData = async () => {
if (!currentTrain.value) return
try {
const data = await TrainApi.exportTrainData(currentTrain.value.id)
download.excel(data, `训练数据_${currentTrain.value.name}.xlsx`)
message.success('导出成功')
} catch (error) {
console.error('导出失败:', error)
message.error('导出失败')
}
}
/** 格式化时间 */
const formatTime = (timestamp: number) => {
return new Date(timestamp).toLocaleTimeString()
}
/** 获取状态类型 */
const getStatusType = (status: string) => {
const statusMap: Record<string, string> = {
'pending': 'info',
'running': 'warning',
'completed': 'success',
'failed': 'danger'
}
return statusMap[status] || 'info'
}
/** 获取状态文本 */
const getStatusText = (status: string) => {
const statusMap: Record<string, string> = {
'pending': '等待中',
'running': '训练中',
'completed': '已完成',
'failed': '失败'
}
return statusMap[status] || '未知'
}
/** 判断是否有训练结果 */
const hasTrainResult = computed(() => {
return trainData.value &&
trainData.value.round !== undefined &&
trainData.value.roundTotal !== undefined &&
trainData.value.round > 0
})
/** 训练按钮操作 */
const handleTrain = async (row: TrainVO) => {
try {
await TrainApi.handleTrain(row);
} catch (error) {
console.error('训练失败:', error)
}
}
const initTrain = async (row: TrainVO) => {
try {
await TrainApi.InfinityTrain(row);
message.info(`初始化完成: ${row.name}`)
getList()
// TODO:
} catch (error) {
console.error('初始化失败:', error)
}
}
const environmentInquiry = async () => {
try {
await TrainApi.environmentInquiry();
// TODO:
} catch (error) {
console.error('训练失败:', error)
}
}
/** 信息按钮操作 */
const handleInfo = async (row: TrainVO) => {
try {
message.info(`查看训练信息: ${row.name}`)
// TODO:
} catch (error) {
console.error('查看信息失败:', error)
}
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
@ -282,6 +606,117 @@ const handleExport = async () => {
/** 初始化 **/
onMounted(() => {
getProjectList()
getList()
})
</script>
onUnmounted(() => {
stopPolling()
})
</script>
<style scoped>
.train-drawer-content {
display: flex;
flex-direction: column;
height: 100%;
padding: 0 16px;
}
.train-info {
flex-shrink: 0;
}
.train-info h3 {
margin-bottom: 16px;
color: #303133;
}
.train-stats {
display: flex;
flex-direction: column;
gap: 12px;
}
.stat-item {
display: flex;
align-items: center;
gap: 12px;
}
.stat-label {
min-width: 80px;
font-weight: 500;
color: #606266;
}
.stat-value {
font-weight: 600;
color: #303133;
}
.loading-placeholder {
margin-top: 16px;
}
.train-logs {
flex: 1;
margin-top: 24px;
display: flex;
flex-direction: column;
}
.train-logs h4 {
margin-bottom: 12px;
color: #303133;
}
.log-container {
flex: 1;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 8px;
}
.log-content {
display: flex;
flex-direction: column;
gap: 4px;
}
.log-item {
display: flex;
gap: 12px;
padding: 4px 8px;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 12px;
}
.log-item:hover {
background-color: #f5f7fa;
}
.log-time {
color: #909399;
flex-shrink: 0;
}
.log-message {
color: #606266;
word-break: break-all;
}
.empty-logs {
display: flex;
align-items: center;
justify-content: center;
height: 100px;
}
.drawer-footer {
display: flex;
gap: 12px;
padding: 16px 0;
}
</style>
Loading…
Cancel
Save