<!-- 评估配置组件 -->
|
<template>
|
<div class="evaluate-config">
|
<div class="public_card_title">评估配置</div>
|
|
<!-- 设备选择 -->
|
<div class="select_form">
|
<div class="label">设备:</div>
|
<el-select v-model="localDevice" placeholder="请选择" @change="handleDeviceChange">
|
<el-option
|
v-for="device in availableDevices"
|
:key="device.value"
|
:label="device.label"
|
:value="device.value"
|
/>
|
</el-select>
|
</div>
|
|
<!-- 拆分选择 -->
|
<div class="select_form">
|
<div class="label">拆分:</div>
|
<el-select v-model="selectedSplit" placeholder="请选择拆分" @change="handleSplitChange">
|
<el-option
|
v-for="split in availableSplits"
|
:key="split.value"
|
:label="split.label"
|
:value="split.value"
|
/>
|
</el-select>
|
</div>
|
|
<!-- 路径配置 -->
|
<div class="path-config-section">
|
<div class="public_card_title">路径配置:</div>
|
|
<!-- 预训练模型 -->
|
<div class="path_item">
|
<label class="path_label">预训练模型:</label>
|
<div class="path_input_wrapper">
|
<el-input
|
v-model="resumeCheckpoint"
|
placeholder="请选择预训练模型文件"
|
readonly
|
/>
|
<el-button type="" @click="selectResumeCheckpoint" class="icon-button">
|
📂
|
</el-button>
|
</div>
|
</div>
|
|
<!-- 输出路径 -->
|
<div class="path_item">
|
<label class="path_label">输出路径:</label>
|
<div class="path_input_wrapper">
|
<el-input
|
v-model="outputDir"
|
placeholder="请选择输出目录"
|
readonly
|
/>
|
<el-button type="" @click="selectOutputDir" class="icon-button">
|
📂
|
</el-button>
|
</div>
|
</div>
|
|
|
</div>
|
</div>
|
</template>
|
|
<script setup>
|
import { ref, watch, computed } from 'vue'
|
import { ElMessage } from 'element-plus'
|
|
const props = defineProps({
|
selectedDevice: {
|
type: String,
|
default: 'cpu'
|
},
|
fixedParams: {
|
type: Object,
|
default: () => ({
|
resume_checkpoint: "",
|
output_dir: ""
|
}),
|
},
|
project: {
|
type: Object,
|
default: null
|
},
|
selectedSplitId: {
|
type: String,
|
default: ''
|
}
|
})
|
|
const emit = defineEmits(['update:selectedDevice', 'update:fixed-params', 'update:selectedSplit'])
|
|
// 本地设备选择
|
const localDevice = ref(props.selectedDevice)
|
|
// 设备列表
|
const availableDevices = ref([
|
{ label: 'CPU', value: 'cpu' },
|
{ label: 'GPU', value: 'gpu' }
|
])
|
|
// 拆分选择
|
const selectedSplit = ref(props.selectedSplitId)
|
|
// 可用拆分列表(computed从project.splits获取)
|
const availableSplits = computed(() => {
|
if (!props.project?.splits || !Array.isArray(props.project.splits)) {
|
return []
|
}
|
|
return props.project.splits.map(split => ({
|
label: split.name || split.title || split.id,
|
value: split.id
|
}))
|
})
|
|
// 路径参数
|
// 设置默认值:如果 props 为空且有项目路径,则使用默认路径
|
const getDefaultResumeCheckpoint = () => {
|
if (props.fixedParams?.resume_checkpoint) {
|
return props.fixedParams.resume_checkpoint
|
}
|
// 如果 props 为空,使用项目路径下的默认路径
|
if (props.project?.path) {
|
return `${props.project.path}/models/checkpoints/best_model.pth`
|
}
|
return ""
|
}
|
|
const getDefaultOutputDir = () => {
|
if (props.fixedParams?.output_dir) {
|
return props.fixedParams.output_dir
|
}
|
// 如果 props 为空,使用项目路径下的默认路径
|
if (props.project?.path) {
|
return `${props.project.path}/models/checkpoints`
|
}
|
return ""
|
}
|
|
const resumeCheckpoint = ref(getDefaultResumeCheckpoint())
|
const outputDir = ref(getDefaultOutputDir())
|
|
// 文件存在性检查
|
const modelFileExists = ref(false)
|
|
// 监听props变化
|
watch(() => props.selectedDevice, (newVal) => {
|
localDevice.value = newVal
|
})
|
|
watch(() => props.selectedSplitId, (newVal) => {
|
selectedSplit.value = newVal
|
})
|
|
// 监听项目变化,更新默认路径
|
watch(() => props.project?.path, (newPath, oldPath) => {
|
// 只有项目路径发生变化时才更新
|
if (newPath && newPath !== oldPath) {
|
// 如果当前路径为空或者是旧项目的路径,更新为新项目的默认路径
|
if (!resumeCheckpoint.value || (oldPath && resumeCheckpoint.value.includes(oldPath))) {
|
resumeCheckpoint.value = `${newPath}/models/checkpoints/best_model.pth`
|
}
|
if (!outputDir.value || (oldPath && outputDir.value.includes(oldPath))) {
|
outputDir.value = `${newPath}/models/checkpoints`
|
}
|
}
|
}, { immediate: false })
|
|
// 监听固定参数props变化
|
// 注意:只有当 props 中的值不为空时才更新本地值,避免清空已有的默认值
|
watch(() => props.fixedParams, (newVal) => {
|
if (newVal) {
|
// 只有 props 的值不为空字符串时才更新,避免清空已有值
|
if (newVal.resume_checkpoint !== undefined && newVal.resume_checkpoint !== "") {
|
resumeCheckpoint.value = newVal.resume_checkpoint
|
}
|
if (newVal.output_dir !== undefined && newVal.output_dir !== "") {
|
outputDir.value = newVal.output_dir
|
}
|
}
|
}, { deep: true, immediate: true })
|
|
// 监听路径参数变化并触发更新
|
watch([resumeCheckpoint, outputDir], () => {
|
emit("update:fixed-params", {
|
resume_checkpoint: resumeCheckpoint.value,
|
output_dir: outputDir.value
|
})
|
})
|
|
// 处理设备变化
|
const handleDeviceChange = (value) => {
|
emit('update:selectedDevice', value)
|
}
|
|
// 处理拆分变化
|
const handleSplitChange = (value) => {
|
console.log('[EvaluateConfig] 切换拆分:', value)
|
emit('update:selectedSplit', value)
|
// 通知父组件拆分已变更,需要更新相关数据
|
}
|
|
// 选择预训练模型
|
const selectResumeCheckpoint = async () => {
|
// @ts-ignore
|
if (window.electronAPI && window.electronAPI.showOpenDialog) {
|
try {
|
// @ts-ignore
|
const result = await window.electronAPI.showOpenDialog({
|
properties: ["openFile"],
|
filters: [
|
{ name: "Model Files", extensions: ["pth", "pt", "onnx", "pb", "h5", "ckpt"] },
|
{ name: "All Files", extensions: ["*"] },
|
],
|
})
|
|
if (!result.canceled && result.filePaths && result.filePaths.length > 0) {
|
resumeCheckpoint.value = result.filePaths[0]
|
}
|
} catch (error) {
|
console.error("选择预训练模型失败:", error)
|
ElMessage.error("选择预训练模型失败")
|
}
|
} else {
|
ElMessage.warning("文件选择功能不可用")
|
}
|
}
|
|
// 选择输出目录
|
const selectOutputDir = async () => {
|
// @ts-ignore
|
if (window.electronAPI && window.electronAPI.showOpenDialog) {
|
try {
|
// @ts-ignore
|
const result = await window.electronAPI.showOpenDialog({
|
properties: ["openDirectory", "createDirectory"],
|
})
|
|
if (!result.canceled && result.filePaths && result.filePaths.length > 0) {
|
outputDir.value = result.filePaths[0]
|
}
|
} catch (error) {
|
console.error("选择输出目录失败:", error)
|
ElMessage.error("选择输出目录失败")
|
}
|
} else {
|
ElMessage.warning("目录选择功能不可用")
|
}
|
}
|
|
// 检查模型文件是否存在
|
const checkModelFileExists = async (filePath) => {
|
if (!filePath || !filePath.trim()) {
|
modelFileExists.value = false
|
return false
|
}
|
|
try {
|
// @ts-ignore
|
if (window.electronAPI && window.electronAPI.pathExists) {
|
// @ts-ignore
|
const exists = await window.electronAPI.pathExists(filePath)
|
modelFileExists.value = exists
|
return exists
|
} else {
|
modelFileExists.value = false
|
return false
|
}
|
} catch (error) {
|
console.error("检查模型文件存在性失败:", error)
|
modelFileExists.value = false
|
return false
|
}
|
}
|
|
// 监听预训练模型路径变化,自动检查文件存在性
|
watch(resumeCheckpoint, async (newPath) => {
|
if (newPath && newPath.trim()) {
|
await checkModelFileExists(newPath)
|
} else {
|
modelFileExists.value = false
|
}
|
}, { immediate: true })
|
|
// 验证评估配置是否有效
|
const validateEvaluationConfig = async () => {
|
const validationResult = {
|
isValid: true,
|
errors: []
|
}
|
|
// 检查是否配置了预训练模型
|
if (!resumeCheckpoint.value || !resumeCheckpoint.value.trim()) {
|
validationResult.isValid = false
|
validationResult.errors.push('请选择预训练模型文件')
|
}
|
|
// 检查输出路径
|
if (!outputDir.value || !outputDir.value.trim()) {
|
validationResult.isValid = false
|
validationResult.errors.push('输出路径不能为空,请选择输出目录')
|
}
|
|
return validationResult
|
}
|
|
// 暴露验证方法给父组件
|
defineExpose({
|
validateEvaluationConfig,
|
checkModelFileExists,
|
modelFileExists: computed(() => modelFileExists.value)
|
})
|
|
</script>
|
|
<style scoped lang="scss">
|
.evaluate-config {
|
.select_form {
|
display: flex;
|
align-items: center;
|
margin-top: 10px;
|
|
.label {
|
font-weight: 400;
|
font-size: 12px;
|
color: #303133;
|
line-height: 12px;
|
margin-right: 8px;
|
flex-shrink: 0;
|
}
|
|
:deep(.el-select) {
|
flex: 1;
|
|
.el-select__wrapper {
|
padding: 4px 8px;
|
min-height: 28px;
|
background: #FFFFFF;
|
border: 1px solid #DCDFE6;
|
border-radius: 3px;
|
box-shadow: none;
|
transition: border-color 0.2s;
|
|
&:hover {
|
border-color: #C0C4CC;
|
}
|
|
&.is-focused {
|
border-color: #409EFF;
|
}
|
}
|
|
.el-select__placeholder {
|
font-size: 12px;
|
color: #C0C4CC;
|
}
|
|
.el-select__selected-item {
|
font-size: 12px;
|
color: #303133;
|
}
|
}
|
}
|
|
.path-config-section {
|
margin-top: 20px;
|
}
|
|
.select_subhead {
|
margin-bottom: 12px;
|
font-weight: 400;
|
font-size: 12px;
|
color: #303133;
|
line-height: 12px;
|
}
|
|
.path_item {
|
display: flex;
|
align-items: center;
|
margin-top: 10px;
|
margin-bottom: 10px;
|
|
&:last-child {
|
margin-bottom: 0;
|
}
|
|
.path_label {
|
font-weight: 400;
|
font-size: 12px;
|
color: #606266;
|
line-height: 28px;
|
width: 80px;
|
flex-shrink: 0;
|
padding-right: 8px;
|
}
|
|
.path_input_wrapper {
|
flex: 1;
|
display: flex;
|
gap: 4px;
|
align-items: center;
|
|
:deep(.el-input) {
|
flex: 1;
|
|
.el-input__wrapper {
|
padding: 4px 8px;
|
min-height: 28px;
|
background: #FFFFFF;
|
border: 1px solid #DCDFE6;
|
border-radius: 3px;
|
box-shadow: none;
|
transition: border-color 0.2s;
|
font-size: 12px;
|
|
&:hover {
|
border-color: #C0C4CC;
|
}
|
|
&.is-focus {
|
border-color: #409EFF;
|
}
|
}
|
|
.el-input__inner {
|
font-size: 12px;
|
height: 20px;
|
line-height: 20px;
|
color: #303133;
|
}
|
}
|
|
// 图标按钮样式
|
.icon-button {
|
padding: 0 6px;
|
font-size: 16px;
|
height: 28px;
|
line-height: 28px;
|
min-width: auto;
|
border: none;
|
background: transparent;
|
color: #409EFF;
|
|
&:hover {
|
background: #F0F2F5;
|
border-radius: 3px;
|
}
|
}
|
}
|
}
|
}
|
</style>
|