|
|
|
|
@ -1,10 +1,9 @@
|
|
|
|
|
<template>
|
|
|
|
|
<div class="subpage-layout">
|
|
|
|
|
<div class="layout-grid">
|
|
|
|
|
|
|
|
|
|
<!-- 左侧:任务栏 -->
|
|
|
|
|
<aside class="grid-sidebar">
|
|
|
|
|
<TaskSideBar :tasks="tasks" />
|
|
|
|
|
<TaskSideBar />
|
|
|
|
|
</aside>
|
|
|
|
|
|
|
|
|
|
<!-- 右侧:主操作卡片 -->
|
|
|
|
|
@ -15,87 +14,172 @@
|
|
|
|
|
<h2>{{ pageTitle }}</h2>
|
|
|
|
|
<p class="subtitle">Validation Task Setup</p>
|
|
|
|
|
</div>
|
|
|
|
|
<span class="tag">Analysis</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="card-body">
|
|
|
|
|
<p class="desc-text">选择已完成的防护任务作为数据源,对其进行攻击模拟或指标计算,以验证防护效果。</p>
|
|
|
|
|
|
|
|
|
|
<!-- === 场景 1: 微调验证 === -->
|
|
|
|
|
<div v-if="subpageType === 'fine-tuning'" class="form-wrapper">
|
|
|
|
|
<!-- Tab 切换 -->
|
|
|
|
|
<div class="mode-tabs">
|
|
|
|
|
<button class="tab-btn" :class="{ active: finetuneMode === 'task' }" @click="finetuneMode = 'task'">
|
|
|
|
|
<i class="fas fa-history"></i> 从现有加噪任务
|
|
|
|
|
</button>
|
|
|
|
|
<button class="tab-btn vip" :class="{ active: finetuneMode === 'upload' }" @click="finetuneMode = 'upload'">
|
|
|
|
|
<i class="fas fa-upload"></i> 自定义上传 (VIP)
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="form-wrapper">
|
|
|
|
|
|
|
|
|
|
<!-- 1. 任务名 -->
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label>验证任务名称</label>
|
|
|
|
|
<input type="text" v-model="formData.taskName" class="ui-input" placeholder="例如:第1批次效果验证..." />
|
|
|
|
|
<!-- 模式 A: 从现有任务 -->
|
|
|
|
|
<div v-if="finetuneMode === 'task'" class="mode-content">
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label>1. 选取已有加噪数据源 (Completed Only)</label>
|
|
|
|
|
<div class="source-selector">
|
|
|
|
|
<div class="source-trigger" @click="isSourceListOpen = !isSourceListOpen">
|
|
|
|
|
<span>{{ currentSourceName }}</span>
|
|
|
|
|
<i class="fas fa-chevron-down"></i>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-if="isSourceListOpen" class="source-list">
|
|
|
|
|
<div v-for="item in candidateTasks" :key="item.id" class="source-item" @click="selectSource(item)">
|
|
|
|
|
<span>#{{ item.id }} - {{ item.name }}</span>
|
|
|
|
|
<span class="s-tag">Done</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- 预览区域 -->
|
|
|
|
|
<div v-if="formData.sourceId" class="single-preview-area fade-in">
|
|
|
|
|
<div v-if="isLoadingImages" class="loading-text"><i class="fas fa-spinner fa-spin"></i> 读取预览...</div>
|
|
|
|
|
<div v-else-if="previewImage" class="preview-content">
|
|
|
|
|
<span class="preview-label">任务首图预览:</span>
|
|
|
|
|
<img :src="previewImage" class="preview-img-sm" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 2. 数据源选择 (核心交互) -->
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label>选择数据源 (原始防护任务)</label>
|
|
|
|
|
<!-- 模式 B: 自定义上传 -->
|
|
|
|
|
<div v-else class="mode-content">
|
|
|
|
|
<div class="upload-zone" @click="triggerFileUpload" :class="{ 'has-file': formData.files.length > 0 }">
|
|
|
|
|
<input type="file" ref="fileInput" @change="handleFileChange" multiple style="display:none" accept="image/*" />
|
|
|
|
|
|
|
|
|
|
<!-- 清空按钮 -->
|
|
|
|
|
<div v-if="formData.files.length > 0" class="clear-file-btn" @click.stop="clearFiles">
|
|
|
|
|
<i class="fas fa-times"></i>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<i class="fas fa-cloud-upload-alt" :class="{ 'success-icon': formData.files.length > 0 }"></i>
|
|
|
|
|
<p v-if="!formData.files.length">点击上传数据集 (多张)</p>
|
|
|
|
|
<p v-else class="file-name">已选择 {{ formData.files.length }} 张图片</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="source-selector">
|
|
|
|
|
<!-- 触发器 -->
|
|
|
|
|
<div class="form-group" style="margin-top: 20px;">
|
|
|
|
|
<label>数据集类型</label>
|
|
|
|
|
<select v-model="formData.dataType" class="ui-input">
|
|
|
|
|
<option :value="1">人脸 (Facial)</option>
|
|
|
|
|
<option :value="2">艺术 (Art)</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 【核心修改】2. 微调配置选择 (3选1) -->
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label>2. 选择微调算法</label>
|
|
|
|
|
<div class="style-selector finetune-selector">
|
|
|
|
|
<div
|
|
|
|
|
class="source-trigger"
|
|
|
|
|
:class="{ active: isSourceListOpen, selected: formData.sourceId }"
|
|
|
|
|
@click="isSourceListOpen = !isSourceListOpen"
|
|
|
|
|
v-for="opt in finetuneOptions"
|
|
|
|
|
:key="opt.id"
|
|
|
|
|
class="style-option"
|
|
|
|
|
:class="{ active: formData.finetuneConfig === opt.id }"
|
|
|
|
|
@click="formData.finetuneConfig = opt.id"
|
|
|
|
|
>
|
|
|
|
|
<div class="trigger-left">
|
|
|
|
|
<div class="icon-box"><i class="fas fa-database"></i></div>
|
|
|
|
|
<div class="trigger-info">
|
|
|
|
|
<span class="t-title">{{ currentSourceName }}</span>
|
|
|
|
|
<span class="t-desc" v-if="formData.sourceId">ID: {{ formData.sourceId }}</span>
|
|
|
|
|
<span class="t-desc" v-else>点击选择任务...</span>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- 选中标记 -->
|
|
|
|
|
<div class="check-mark" v-if="formData.finetuneConfig === opt.id">
|
|
|
|
|
<i class="fas fa-check"></i>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- 内容 -->
|
|
|
|
|
<div class="option-text">
|
|
|
|
|
<span class="opt-title">{{ opt.name }}</span>
|
|
|
|
|
<span class="opt-desc">{{ opt.desc }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<i class="fas fa-chevron-down arrow"></i>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 公共部分 -->
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label>微调任务名称</label>
|
|
|
|
|
<input type="text" v-model="formData.taskName" class="ui-input" placeholder="输入任务名称..." />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 自定义 Prompt -->
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label>自定义提示词 (Optional Prompt)</label>
|
|
|
|
|
<input type="text" v-model="formData.customPrompt" class="ui-input" placeholder="例如: a photo of sks person..." />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 展开的任务列表 -->
|
|
|
|
|
<Transition name="expand">
|
|
|
|
|
<button class="ui-btn gradient rect big-btn" @click="submitTask" :disabled="isSubmitting">
|
|
|
|
|
<i class="fas" :class="isSubmitting ? 'fa-spinner fa-spin' : 'fa-rocket'"></i>
|
|
|
|
|
{{ isSubmitting ? '提交中...' : '提交微调任务' }}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- === 场景 2: 评估与热力图 === -->
|
|
|
|
|
<div v-else class="form-wrapper">
|
|
|
|
|
|
|
|
|
|
<!-- 1. 选择数据源 -->
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label>选择数据源 ({{ subpageType === 'heatmap' ? '已完成的加噪任务' : '微调任务' }})</label>
|
|
|
|
|
<div class="source-selector">
|
|
|
|
|
<div class="source-trigger" @click="isSourceListOpen = !isSourceListOpen">
|
|
|
|
|
<span>{{ currentSourceName }}</span>
|
|
|
|
|
<i class="fas fa-chevron-down"></i>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-if="isSourceListOpen" class="source-list">
|
|
|
|
|
<div class="list-header">可选历史任务</div>
|
|
|
|
|
<div class="list-body">
|
|
|
|
|
<div
|
|
|
|
|
v-for="item in historyTasks"
|
|
|
|
|
:key="item.id"
|
|
|
|
|
class="source-item"
|
|
|
|
|
@click="selectSource(item)"
|
|
|
|
|
>
|
|
|
|
|
<div class="s-info">
|
|
|
|
|
<span class="s-name">{{ item.name }}</span>
|
|
|
|
|
<span class="s-date">{{ item.date }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<span class="s-tag">{{ item.imageCount }}张图</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-for="item in candidateTasks" :key="item.id" class="source-item" @click="selectSource(item)">
|
|
|
|
|
<span>#{{ item.id }} - {{ item.name }}</span>
|
|
|
|
|
<span class="s-tag">Done</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</Transition>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- 评估模式单图预览 -->
|
|
|
|
|
<div v-if="subpageType === 'metrics' && formData.sourceId" class="single-preview-area fade-in">
|
|
|
|
|
<div v-if="isLoadingImages" class="loading-text"><i class="fas fa-spinner fa-spin"></i> 读取预览...</div>
|
|
|
|
|
<div v-else-if="previewImage" class="preview-content">
|
|
|
|
|
<span class="preview-label">任务首图预览:</span>
|
|
|
|
|
<img :src="previewImage" class="preview-img-sm" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 3. 可视化类型 (仅热力图显示) -->
|
|
|
|
|
<div class="form-group" v-if="subpageType === 'heatmap'">
|
|
|
|
|
<label>可视化类型</label>
|
|
|
|
|
<div class="style-selector">
|
|
|
|
|
<div class="style-option" :class="{ active: formData.visType === 'attention' }" @click="formData.visType = 'attention'">
|
|
|
|
|
<i class="fas fa-eye"></i> <span>Attention Map</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="style-option" :class="{ active: formData.visType === 'frequency' }" @click="formData.visType = 'frequency'">
|
|
|
|
|
<i class="fas fa-wave-square"></i> <span>频域分析</span>
|
|
|
|
|
<!-- 【热力图专用】图片选择区域 -->
|
|
|
|
|
<div v-if="subpageType === 'heatmap'" class="form-group fade-in" :class="{ 'disabled': !formData.sourceId }">
|
|
|
|
|
<label>选择目标图片 <span v-if="taskImages.length">({{ taskImages.length }}张)</span></label>
|
|
|
|
|
<div class="image-select-container">
|
|
|
|
|
<div v-if="isLoadingImages" class="loading-box"><i class="fas fa-spinner fa-spin"></i> 加载中...</div>
|
|
|
|
|
<div v-else-if="!formData.sourceId" class="empty-box">请先选择上方的加噪任务</div>
|
|
|
|
|
<div v-else class="image-grid">
|
|
|
|
|
<div v-for="img in taskImages" :key="img.image_id" class="img-item"
|
|
|
|
|
:class="{ active: formData.selectedImageId === img.image_id }"
|
|
|
|
|
@click="selectImage(img)">
|
|
|
|
|
<img :src="img.base64" loading="lazy" />
|
|
|
|
|
<div class="check-overlay"><i class="fas fa-check"></i></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 4. 提交 -->
|
|
|
|
|
<div class="submit-area">
|
|
|
|
|
<button class="ui-btn solid rect big-btn" @click="submitTask">
|
|
|
|
|
<i class="fas fa-play"></i>
|
|
|
|
|
开始验证分析
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<!-- 任务名称 -->
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label>任务名称</label>
|
|
|
|
|
<input type="text" v-model="formData.taskName" class="ui-input" placeholder="输入任务名称..." />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<button class="ui-btn solid rect big-btn" @click="submitTask" :disabled="isSubmitting">
|
|
|
|
|
<i class="fas" :class="isSubmitting ? 'fa-spinner fa-spin' : 'fa-play'"></i>
|
|
|
|
|
{{ isSubmitting ? '提交中...' : '提交任务' }}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</main>
|
|
|
|
|
@ -104,158 +188,311 @@
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
import { ref, computed } from 'vue'
|
|
|
|
|
import { ref, computed, onMounted } from 'vue'
|
|
|
|
|
import { useRoute } from 'vue-router'
|
|
|
|
|
import TaskSideBar from '@/components/TaskSideBar.vue'
|
|
|
|
|
import { useTaskStore } from '@/stores/taskStore'
|
|
|
|
|
import {
|
|
|
|
|
submitFinetuneFromPerturbation,
|
|
|
|
|
submitFinetuneFromUpload,
|
|
|
|
|
submitEvaluateTask,
|
|
|
|
|
submitHeatmapTask,
|
|
|
|
|
startFinetuneTask,
|
|
|
|
|
startEvaluateTask,
|
|
|
|
|
getTaskResultImages // 仅用于部分逻辑,热力图等已改用 image/preview 接口
|
|
|
|
|
} from '@/api/task'
|
|
|
|
|
import { getTaskImagePreview } from '@/api/image'
|
|
|
|
|
import { FINETUNE_MAP } from '@/utils/constants'
|
|
|
|
|
|
|
|
|
|
const route = useRoute()
|
|
|
|
|
const taskStore = useTaskStore()
|
|
|
|
|
const isSourceListOpen = ref(false)
|
|
|
|
|
const fileInput = ref(null)
|
|
|
|
|
|
|
|
|
|
// 模拟左侧任务栏
|
|
|
|
|
const tasks = ref([
|
|
|
|
|
{ id: '3001', status: 'running', progress: 45 },
|
|
|
|
|
{ id: '3002', status: 'waiting', progress: 0 }
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
// 模拟可选的历史任务数据源
|
|
|
|
|
const historyTasks = [
|
|
|
|
|
{ id: '1024', name: '风景图通用防护任务', date: '2023-11-20', imageCount: 12 },
|
|
|
|
|
{ id: '1025', name: '人脸隐私加噪测试', date: '2023-11-21', imageCount: 5 },
|
|
|
|
|
{ id: '1026', name: '艺术风格迁移防御', date: '2023-11-22', imageCount: 8 }
|
|
|
|
|
]
|
|
|
|
|
const subpageType = computed(() => route.params.subpage)
|
|
|
|
|
const pageTitle = computed(() => subpageType.value === 'fine-tuning' ? '微调生图验证' : (subpageType.value === 'heatmap' ? '热力图分析' : '数据指标对比'))
|
|
|
|
|
|
|
|
|
|
const finetuneMode = ref('task')
|
|
|
|
|
const isLoadingImages = ref(false)
|
|
|
|
|
const isSubmitting = ref(false)
|
|
|
|
|
const taskImages = ref([])
|
|
|
|
|
const previewImage = ref(null)
|
|
|
|
|
|
|
|
|
|
const formData = ref({
|
|
|
|
|
taskName: '',
|
|
|
|
|
sourceId: '',
|
|
|
|
|
sourceName: '',
|
|
|
|
|
visType: 'attention'
|
|
|
|
|
finetuneConfig: FINETUNE_MAP.LORA, // 默认 LoRA
|
|
|
|
|
dataType: 1,
|
|
|
|
|
files: [],
|
|
|
|
|
selectedImageId: null,
|
|
|
|
|
customPrompt: ''
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const subpageType = computed(() => route.params.subpage)
|
|
|
|
|
// 【新增】微调选项
|
|
|
|
|
const finetuneOptions = [
|
|
|
|
|
{ id: FINETUNE_MAP.DREAMBOOTH, name: 'DreamBooth', desc: '高质量,耗时较长' },
|
|
|
|
|
{ id: FINETUNE_MAP.LORA, name: 'LoRA', desc: '平衡推荐,速度快' },
|
|
|
|
|
{ id: FINETUNE_MAP.TEXTUAL_INVERSION, name: 'Textual Inv', desc: '轻量级风格' }
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
const pageTitle = computed(() => {
|
|
|
|
|
const map = {
|
|
|
|
|
'tuning': '微调生图验证',
|
|
|
|
|
'metrics': '数据指标对比',
|
|
|
|
|
'heatmap': '热力图分析'
|
|
|
|
|
}
|
|
|
|
|
return map[subpageType.value] || '效果验证'
|
|
|
|
|
const candidateTasks = computed(() => {
|
|
|
|
|
const targetType = (subpageType.value === 'metrics') ? 'finetune' : 'perturbation'
|
|
|
|
|
return taskStore.tasks.filter(t =>
|
|
|
|
|
t.status === 'completed' && t.task_type === targetType
|
|
|
|
|
).map(t => {
|
|
|
|
|
let displayName = `Task #${t.task_id}`
|
|
|
|
|
if (t.description) displayName = t.description
|
|
|
|
|
else if (t.task_type === 'finetune' && t.finetune?.finetune_name) displayName = t.finetune.finetune_name
|
|
|
|
|
else if (t.perturbation?.perturbation_name) displayName = t.perturbation.perturbation_name
|
|
|
|
|
return { id: t.task_id, name: displayName, status: t.status }
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const currentSourceName = computed(() => {
|
|
|
|
|
return formData.value.sourceName || '请选择数据源'
|
|
|
|
|
})
|
|
|
|
|
const currentSourceName = computed(() => formData.value.sourceName || '点击选择...')
|
|
|
|
|
|
|
|
|
|
const selectSource = (item) => {
|
|
|
|
|
const selectSource = async (item) => {
|
|
|
|
|
formData.value.sourceId = item.id
|
|
|
|
|
formData.value.sourceName = item.name
|
|
|
|
|
isSourceListOpen.value = false
|
|
|
|
|
formData.value.selectedImageId = null
|
|
|
|
|
taskImages.value = []
|
|
|
|
|
previewImage.value = null
|
|
|
|
|
isLoadingImages.value = true
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
if (subpageType.value === 'heatmap') {
|
|
|
|
|
const res = await getTaskImagePreview(item.id)
|
|
|
|
|
if (res && res.images) {
|
|
|
|
|
const list = res.images.perturbed || res.images.original || []
|
|
|
|
|
taskImages.value = list.map(img => {
|
|
|
|
|
let rawData = img.data || img.base64 || ''
|
|
|
|
|
if (rawData && !rawData.startsWith('data:image')) {
|
|
|
|
|
rawData = `data:image/jpeg;base64,${rawData}`
|
|
|
|
|
}
|
|
|
|
|
return { ...img, image_id: img.image_id || img.id, base64: rawData }
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
// 微调或评估预览
|
|
|
|
|
const res = await getTaskImagePreview(item.id) // 统一使用轻量接口
|
|
|
|
|
if (res && res.images) {
|
|
|
|
|
// 尝试获取一张代表图
|
|
|
|
|
let targetImg = null
|
|
|
|
|
if (subpageType.value === 'fine-tuning') {
|
|
|
|
|
// 加噪任务结果:原图或加噪图
|
|
|
|
|
targetImg = res.images.perturbed?.[0] || res.images.original?.[0]
|
|
|
|
|
} else {
|
|
|
|
|
// 微调任务结果:原图生成或加噪生成
|
|
|
|
|
targetImg = res.images.original_generate?.[0] || res.images.perturbed_generate?.[0] || res.images.uploaded_generate?.[0]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (targetImg) {
|
|
|
|
|
let raw = targetImg.data || targetImg.base64 || ''
|
|
|
|
|
if (raw && !raw.startsWith('data:image')) raw = `data:image/jpeg;base64,${raw}`
|
|
|
|
|
previewImage.value = raw
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('获取图片失败:', error)
|
|
|
|
|
} finally {
|
|
|
|
|
isLoadingImages.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const selectImage = (img) => formData.value.selectedImageId = img.image_id
|
|
|
|
|
const triggerFileUpload = () => fileInput.value.click()
|
|
|
|
|
const handleFileChange = (e) => formData.value.files = Array.from(e.target.files)
|
|
|
|
|
const clearFiles = () => {
|
|
|
|
|
formData.value.files = []
|
|
|
|
|
if (fileInput.value) fileInput.value.value = ''
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const submitTask = () => {
|
|
|
|
|
if (!formData.value.taskName) return alert('请填写任务名称')
|
|
|
|
|
if (!formData.value.sourceId) return alert('请选择数据源')
|
|
|
|
|
|
|
|
|
|
console.log(`Submitting Validation [${subpageType.value}]:`, formData.value)
|
|
|
|
|
alert('验证任务已提交 (模拟)!')
|
|
|
|
|
const submitTask = async () => {
|
|
|
|
|
if (!formData.value.taskName) return alert('请输入任务名称')
|
|
|
|
|
isSubmitting.value = true
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
let taskId = null
|
|
|
|
|
|
|
|
|
|
// 1. 微调逻辑
|
|
|
|
|
if (subpageType.value === 'fine-tuning') {
|
|
|
|
|
if (finetuneMode.value === 'task') {
|
|
|
|
|
if (!formData.value.sourceId) throw new Error('请选择数据源')
|
|
|
|
|
const payload = {
|
|
|
|
|
perturbation_task_id: formData.value.sourceId,
|
|
|
|
|
finetune_configs_id: formData.value.finetuneConfig, // 使用用户选择的算法
|
|
|
|
|
finetune_name: formData.value.taskName,
|
|
|
|
|
custom_prompt: formData.value.customPrompt || undefined
|
|
|
|
|
}
|
|
|
|
|
const res = await submitFinetuneFromPerturbation(payload)
|
|
|
|
|
taskId = res.task?.task_id || res.job_id
|
|
|
|
|
} else {
|
|
|
|
|
if (!formData.value.files.length) throw new Error('请上传图片')
|
|
|
|
|
const form = new FormData()
|
|
|
|
|
form.append('finetune_configs_id', formData.value.finetuneConfig) // 使用用户选择的算法
|
|
|
|
|
form.append('data_type_id', formData.value.dataType)
|
|
|
|
|
form.append('finetune_name', formData.value.taskName)
|
|
|
|
|
form.append('description', '[Upload] ' + formData.value.taskName)
|
|
|
|
|
if(formData.value.customPrompt) form.append('custom_prompt', formData.value.customPrompt)
|
|
|
|
|
formData.value.files.forEach(f => form.append('files', f))
|
|
|
|
|
const res = await submitFinetuneFromUpload(form)
|
|
|
|
|
taskId = res.task?.task_id || res.job_id
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (taskId) {
|
|
|
|
|
await startFinetuneTask(taskId)
|
|
|
|
|
alert('微调任务已创建并启动!')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. 评估逻辑
|
|
|
|
|
else if (subpageType.value === 'metrics') {
|
|
|
|
|
if (!formData.value.sourceId) throw new Error('请选择微调任务')
|
|
|
|
|
const res = await submitEvaluateTask({
|
|
|
|
|
finetune_task_id: formData.value.sourceId,
|
|
|
|
|
evaluate_name: formData.value.taskName
|
|
|
|
|
})
|
|
|
|
|
taskId = res.task?.task_id || res.job_id
|
|
|
|
|
if (taskId) await startEvaluateTask(taskId)
|
|
|
|
|
alert('评估任务已创建并启动!')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3. 热力图逻辑
|
|
|
|
|
else if (subpageType.value === 'heatmap') {
|
|
|
|
|
if (!formData.value.sourceId) throw new Error('请选择加噪任务')
|
|
|
|
|
if (!formData.value.selectedImageId) throw new Error('请选择一张目标图片')
|
|
|
|
|
const res = await submitHeatmapTask({
|
|
|
|
|
perturbation_task_id: formData.value.sourceId,
|
|
|
|
|
perturbed_image_id: formData.value.selectedImageId,
|
|
|
|
|
heatmap_name: formData.value.taskName,
|
|
|
|
|
description: formData.value.taskName
|
|
|
|
|
})
|
|
|
|
|
alert(res.message || '热力图任务已创建并启动!')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 清空部分表单
|
|
|
|
|
formData.value.taskName = ''
|
|
|
|
|
formData.value.customPrompt = ''
|
|
|
|
|
clearFiles()
|
|
|
|
|
taskStore.fetchTasks()
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(error)
|
|
|
|
|
alert(error.message || '提交失败')
|
|
|
|
|
} finally {
|
|
|
|
|
isSubmitting.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onMounted(() => taskStore.fetchTasks())
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
/* === 基础布局复用 (Page1) === */
|
|
|
|
|
.subpage-layout { width: 100%; height: 100%; padding: 40px; display: flex; justify-content: center; align-items: center; background: var(--color-bg-primary); }
|
|
|
|
|
.layout-grid { display: grid; grid-template-columns: 300px 1fr; gap: 30px; width: 100%; max-width: 1400px; height: 85vh; }
|
|
|
|
|
/* === 基础布局 === */
|
|
|
|
|
.subpage-layout { width: 100%; height: 100%; padding: 20px; display: flex; justify-content: center; align-items: center; background: var(--color-bg-primary); overflow-y: auto; }
|
|
|
|
|
.layout-grid { display: grid; grid-template-columns: 280px 1fr; gap: 20px; width: 100%; max-width: 1400px; height: 95%; max-height: 95vh; min-height: 0; }
|
|
|
|
|
.grid-sidebar { height: 100%; overflow: hidden; }
|
|
|
|
|
.grid-main { height: 100%; min-width: 0; }
|
|
|
|
|
.content-card { height: 100%; display: flex; flex-direction: column; padding: 0; background: #ffffff; }
|
|
|
|
|
.card-header { padding: 30px 40px; border-bottom: 1px solid rgba(0,0,0,0.05); display: flex; justify-content: space-between; align-items: flex-start; }
|
|
|
|
|
.grid-main { height: 100%; min-width: 0; display: flex; flex-direction: column; }
|
|
|
|
|
.content-card { height: 100%; display: flex; flex-direction: column; padding: 0; background: #ffffff; overflow: hidden; }
|
|
|
|
|
.card-header { flex: 0 0 auto; padding: 20px 30px; border-bottom: 1px solid rgba(0,0,0,0.05); display: flex; justify-content: space-between; align-items: flex-start; }
|
|
|
|
|
.subtitle { color: var(--color-text-muted); font-size: 0.9rem; margin-top: 5px; }
|
|
|
|
|
.tag { background: var(--color-contrast-dark); color: #fff; padding: 6px 16px; border-radius: 20px; font-weight: 700; font-size: 0.8rem; }
|
|
|
|
|
.card-body { padding: 40px; flex: 1; overflow-y: auto; }
|
|
|
|
|
.desc-text { color: var(--color-text-main); margin-bottom: 40px; padding: 15px 20px; background: rgba(24, 40, 59, 0.03); border-left: 4px solid var(--color-contrast-dark); border-radius: 4px; }
|
|
|
|
|
.form-wrapper { display: flex; flex-direction: column; gap: 30px; }
|
|
|
|
|
.form-group label { display: block; font-size: 1rem; font-weight: 600; margin-bottom: 12px; color: var(--color-text-main); }
|
|
|
|
|
.ui-input { width: 100%; padding: 16px; border: 1px solid #e0e0e0; border-radius: 12px; font-size: 1rem; background: #f8f9fa; outline: none; transition: all 0.2s; }
|
|
|
|
|
.ui-input:focus { background: #fff; border-color: var(--color-contrast-dark); }
|
|
|
|
|
.submit-area { margin-top: 20px; }
|
|
|
|
|
.big-btn { width: 100%; height: 60px; font-size: 1.2rem; border-radius: 16px; }
|
|
|
|
|
|
|
|
|
|
/* === 验证页面特有样式 === */
|
|
|
|
|
|
|
|
|
|
/* 数据源选择器 */
|
|
|
|
|
.source-selector {
|
|
|
|
|
position: relative;
|
|
|
|
|
width: 100%;
|
|
|
|
|
.card-body { padding: 30px; flex: 1; overflow-y: auto; min-height: 0; padding-bottom: 60px; }
|
|
|
|
|
|
|
|
|
|
/* === 表单 === */
|
|
|
|
|
.form-wrapper { display: flex; flex-direction: column; gap: 25px; }
|
|
|
|
|
.form-group label { display: block; font-weight: 600; margin-bottom: 10px; }
|
|
|
|
|
.ui-input { width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 8px; min-width: 0; }
|
|
|
|
|
.big-btn { width: 100%; height: 50px; font-size: 1.1rem; border-radius: 10px; margin-top: 10px; }
|
|
|
|
|
.fade-in { animation: fadeIn 0.3s ease-in-out; }
|
|
|
|
|
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
|
|
|
|
|
|
|
|
|
/* === 模式切换 Tab === */
|
|
|
|
|
.mode-tabs { display: flex; gap: 15px; margin-bottom: 25px; border-bottom: 2px solid #eee; padding-bottom: 10px; flex-wrap: wrap; }
|
|
|
|
|
.tab-btn { padding: 10px 20px; border: none; background: transparent; cursor: pointer; font-size: 1rem; color: #999; font-weight: 600; border-radius: 8px; transition: all 0.2s; white-space: nowrap; }
|
|
|
|
|
.tab-btn.active { background: var(--color-contrast-dark); color: #fff; }
|
|
|
|
|
.tab-btn.vip { color: var(--color-accent-secondary); }
|
|
|
|
|
.tab-btn.vip.active { background: linear-gradient(135deg, #FFD166, #FF9F1C); color: #fff; }
|
|
|
|
|
|
|
|
|
|
/* === 风格/算法选择器 === */
|
|
|
|
|
.style-selector {
|
|
|
|
|
display: grid;
|
|
|
|
|
gap: 15px;
|
|
|
|
|
}
|
|
|
|
|
/* 微调配置专用 (3列自适应) */
|
|
|
|
|
.finetune-selector {
|
|
|
|
|
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.source-trigger {
|
|
|
|
|
border: 1px solid #e0e0e0;
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
padding: 15px 20px;
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
background: #f8f9fa;
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
.style-option {
|
|
|
|
|
position: relative; background: #f8f9fa; border-radius: 12px; padding: 15px;
|
|
|
|
|
cursor: pointer; display: flex; flex-direction: column; gap: 5px;
|
|
|
|
|
border: 2px solid transparent; transition: all 0.2s;
|
|
|
|
|
}
|
|
|
|
|
.style-option:hover { background: #f1f3f5; }
|
|
|
|
|
.style-option.active {
|
|
|
|
|
background: #fff; border-color: var(--color-contrast-dark);
|
|
|
|
|
box-shadow: 0 4px 15px rgba(0,0,0,0.05);
|
|
|
|
|
}
|
|
|
|
|
.check-mark { position: absolute; top: 8px; right: 8px; color: var(--color-contrast-dark); font-size: 0.9rem; }
|
|
|
|
|
.option-text { display: flex; flex-direction: column; }
|
|
|
|
|
.opt-title { font-weight: 700; font-size: 0.95rem; }
|
|
|
|
|
.opt-desc { font-size: 0.75rem; color: #888; margin-top: 2px; }
|
|
|
|
|
|
|
|
|
|
.source-trigger:hover { background: #fff; border-color: #ccc; }
|
|
|
|
|
.source-trigger.active { background: #fff; border-color: var(--color-contrast-dark); box-shadow: 0 0 0 4px rgba(24, 40, 59, 0.05); }
|
|
|
|
|
.source-trigger.selected .icon-box { background: var(--color-contrast-dark); color: #fff; }
|
|
|
|
|
|
|
|
|
|
.trigger-left { display: flex; align-items: center; gap: 15px; }
|
|
|
|
|
.icon-box { width: 40px; height: 40px; background: #e9ecef; border-radius: 10px; display: flex; align-items: center; justify-content: center; color: var(--color-text-muted); transition: all 0.2s; }
|
|
|
|
|
.trigger-info { display: flex; flex-direction: column; }
|
|
|
|
|
.t-title { font-weight: 600; font-size: 1rem; }
|
|
|
|
|
.t-desc { font-size: 0.8rem; color: var(--color-text-muted); }
|
|
|
|
|
|
|
|
|
|
.arrow { transition: transform 0.3s; color: var(--color-text-muted); }
|
|
|
|
|
.source-trigger.active .arrow { transform: rotate(180deg); }
|
|
|
|
|
|
|
|
|
|
/* 下拉列表 */
|
|
|
|
|
.source-list {
|
|
|
|
|
margin-top: 10px;
|
|
|
|
|
background: #fff;
|
|
|
|
|
border: 1px solid rgba(0,0,0,0.05);
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
/* === 上传与选择器 === */
|
|
|
|
|
.upload-zone { position: relative; border: 2px dashed #ddd; border-radius: 12px; padding: 30px; text-align: center; cursor: pointer; transition: all 0.2s; background: #fcfcfc; }
|
|
|
|
|
.upload-zone:hover { border-color: var(--color-contrast-dark); background: #fff; }
|
|
|
|
|
.upload-zone.has-file { border-style: solid; border-color: #2e7d32; background: #f1f8e9; }
|
|
|
|
|
.upload-icon { font-size: 2rem; color: var(--color-text-muted); margin-bottom: 10px; }
|
|
|
|
|
.success-icon { color: #2e7d32 !important; }
|
|
|
|
|
.file-name { font-weight: 700; color: #2e7d32; }
|
|
|
|
|
.clear-file-btn {
|
|
|
|
|
position: absolute; top: 10px; right: 10px; width: 30px; height: 30px;
|
|
|
|
|
background: rgba(220, 53, 69, 0.1); color: #dc3545;
|
|
|
|
|
border-radius: 50%; display: flex; align-items: center; justify-content: center;
|
|
|
|
|
transition: all 0.2s; z-index: 10;
|
|
|
|
|
}
|
|
|
|
|
.clear-file-btn:hover { background: #dc3545; color: #fff; transform: scale(1.1); }
|
|
|
|
|
.clear-file-btn i { font-size: 1rem; margin: 0; color: inherit; }
|
|
|
|
|
|
|
|
|
|
.list-header {
|
|
|
|
|
padding: 10px 20px;
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
background: #f8f9fa;
|
|
|
|
|
border-bottom: 1px solid #eee;
|
|
|
|
|
.source-selector { position: relative; }
|
|
|
|
|
.source-trigger { border: 1px solid #eee; padding: 12px; border-radius: 8px; display: flex; justify-content: space-between; cursor: pointer; background: #f9f9f9; }
|
|
|
|
|
.source-list { position: absolute; top: 100%; width: 100%; background: #fff; border: 1px solid #eee; box-shadow: 0 5px 20px rgba(0,0,0,0.1); z-index: 10; max-height: 200px; overflow-y: auto; }
|
|
|
|
|
.source-item { padding: 10px; cursor: pointer; display: flex; justify-content: space-between; }
|
|
|
|
|
.source-item:hover { background: #f0f0f0; }
|
|
|
|
|
.s-tag { font-size: 0.8rem; background: #e8f5e9; color: #2e7d32; padding: 2px 6px; border-radius: 4px; }
|
|
|
|
|
|
|
|
|
|
/* === 图片选择网格 === */
|
|
|
|
|
.image-select-container {
|
|
|
|
|
border: 2px dashed #e0e0e0; border-radius: 12px; padding: 15px; background: #fafafa;
|
|
|
|
|
min-height: 180px; max-height: 40vh; overflow-y: auto; display: flex; flex-direction: column;
|
|
|
|
|
}
|
|
|
|
|
.disabled .image-select-container { opacity: 0.6; pointer-events: none; }
|
|
|
|
|
.loading-box, .empty-box { flex: 1; display: flex; align-items: center; justify-content: center; color: #999; font-size: 0.9rem; min-height: 120px; }
|
|
|
|
|
.image-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); gap: 12px; width: 100%; }
|
|
|
|
|
.img-item { aspect-ratio: 1; border-radius: 8px; overflow: hidden; position: relative; cursor: pointer; border: 3px solid transparent; transition: all 0.2s; background: #eee; }
|
|
|
|
|
.img-item img { width: 100%; height: 100%; object-fit: cover; }
|
|
|
|
|
.img-item:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
|
|
|
|
|
.img-item.active { border-color: var(--color-accent-secondary); transform: scale(0.95); }
|
|
|
|
|
.check-overlay { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(255, 159, 28, 0.4); display: none; align-items: center; justify-content: center; color: white; font-size: 1.5rem; backdrop-filter: blur(1px); }
|
|
|
|
|
.img-item.active .check-overlay { display: flex; }
|
|
|
|
|
|
|
|
|
|
.single-preview-area { margin-top: 10px; background: #f8f9fa; border-radius: 8px; padding: 10px; border: 1px dashed #ddd; display: flex; align-items: center; justify-content: center; min-height: 80px; }
|
|
|
|
|
.preview-content { display: flex; align-items: center; gap: 15px; width: 100%; }
|
|
|
|
|
.preview-label { font-weight: 600; font-size: 0.9rem; color: #666; white-space: nowrap; }
|
|
|
|
|
.preview-img-sm { height: 60px; width: 60px; object-fit: cover; border-radius: 6px; border: 1px solid #eee; }
|
|
|
|
|
|
|
|
|
|
.source-item {
|
|
|
|
|
padding: 15px 20px;
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: background 0.1s;
|
|
|
|
|
border-bottom: 1px solid #f8f9fa;
|
|
|
|
|
/* === 响应式适配 (High Zoom / Mobile) === */
|
|
|
|
|
@media (max-width: 900px) {
|
|
|
|
|
.subpage-layout { padding: 15px; height: auto; min-height: 100vh; align-items: flex-start; }
|
|
|
|
|
.layout-grid { grid-template-columns: 1fr; display: flex; flex-direction: column; height: auto; max-height: none; gap: 15px; }
|
|
|
|
|
.grid-sidebar { display: none; }
|
|
|
|
|
.content-card { height: auto; overflow: visible; }
|
|
|
|
|
.card-body { overflow: visible; padding: 20px; }
|
|
|
|
|
.mode-tabs { flex-direction: column; gap: 10px; }
|
|
|
|
|
.tab-btn { width: 100%; text-align: center; }
|
|
|
|
|
.style-selector { grid-template-columns: 1fr; }
|
|
|
|
|
}
|
|
|
|
|
.source-item:last-child { border-bottom: none; }
|
|
|
|
|
.source-item:hover { background: #f0f7ff; }
|
|
|
|
|
|
|
|
|
|
.s-info { display: flex; flex-direction: column; }
|
|
|
|
|
.s-name { font-weight: 600; color: var(--color-text-main); }
|
|
|
|
|
.s-date { font-size: 0.8rem; color: var(--color-text-muted); }
|
|
|
|
|
.s-tag { font-size: 0.8rem; padding: 2px 8px; background: #e9ecef; border-radius: 4px; color: var(--color-text-muted); }
|
|
|
|
|
|
|
|
|
|
/* 动画 */
|
|
|
|
|
.expand-enter-active, .expand-leave-active { transition: all 0.2s ease; max-height: 300px; opacity: 1; }
|
|
|
|
|
.expand-enter-from, .expand-leave-to { max-height: 0; opacity: 0; }
|
|
|
|
|
|
|
|
|
|
/* 简单选项 */
|
|
|
|
|
.style-selector { display: flex; gap: 20px; }
|
|
|
|
|
.style-option { flex: 1; padding: 15px; border-radius: 12px; background: #f8f9fa; border: 1px solid #e0e0e0; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 10px; font-weight: 600; transition: all 0.2s; }
|
|
|
|
|
.style-option:hover { background: #fff; box-shadow: 0 4px 10px rgba(0,0,0,0.05); }
|
|
|
|
|
.style-option.active { background: var(--color-contrast-dark); color: #fff; border-color: var(--color-contrast-dark); }
|
|
|
|
|
</style>
|