Compare commits

...

4 Commits

Author SHA1 Message Date
53e8fbb4dd 通用分割 2026-02-18 16:55:17 +08:00
f7c73fa57e 通用分割 2026-02-18 16:54:52 +08:00
bad6bfa34b 优化手机端 2026-02-18 16:48:48 +08:00
054e720e39 优化手机端 2026-02-18 14:50:21 +08:00

View File

@@ -135,16 +135,29 @@
<!-- Main App Interface -->
<div v-if="isLoggedIn" class="flex flex-1 h-full overflow-hidden bg-slate-50">
<!-- Mobile Overlay -->
<transition name="fade">
<div v-if="isSidebarOpen" @click="isSidebarOpen = false" class="fixed inset-0 bg-slate-900/50 z-40 lg:hidden backdrop-blur-sm"></div>
</transition>
<!-- Sidebar -->
<aside class="w-72 bg-white border-r border-slate-200 flex flex-col shadow-sm z-10">
<div class="p-8 pb-4">
<aside :class="[
'fixed inset-y-0 left-0 z-50 w-72 bg-white border-r border-slate-200 flex flex-col shadow-sm transition-transform duration-300 ease-in-out lg:static lg:translate-x-0',
isSidebarOpen ? 'translate-x-0' : '-translate-x-full'
]">
<!-- Close Button (Mobile Only) -->
<button @click="isSidebarOpen = false" class="absolute top-4 right-4 lg:hidden text-slate-400 hover:text-slate-600 p-2 rounded-full hover:bg-slate-100 transition-colors z-20">
<i class="fas fa-times text-lg"></i>
</button>
<div class="p-8 pb-4 relative">
<div class="flex items-center gap-3 text-slate-800 mb-8">
<div class="w-10 h-10 bg-gradient-to-br from-blue-600 to-indigo-600 rounded-lg flex items-center justify-center shadow-md shadow-blue-200">
<i class="fas fa-layer-group text-white text-lg"></i>
</div>
<div>
<h2 class="font-bold text-lg leading-tight tracking-tight">SAM3 Admin</h2>
<p class="text-[10px] text-slate-400 font-bold tracking-widest uppercase mt-0.5">Quantum Track AI</p>
<p class="text-xs text-slate-400 font-bold tracking-widest uppercase mt-0.5">Quantum Track AI</p>
</div>
</div>
@@ -160,6 +173,11 @@
<i class="fas fa-star w-5 text-center transition-colors group-hover:text-blue-600" :class="currentTab === 'tarot' ? 'text-blue-600' : 'text-slate-400'"></i>
<span class="font-medium">塔罗牌识别</span>
</a>
<a href="#" @click.prevent="switchTab('segment')" :class="{ 'active': currentTab === 'segment' }"
class="nav-link flex items-center gap-3 px-4 py-3 text-slate-600 hover:bg-slate-50 hover:text-blue-600 rounded-xl transition-all duration-200 group">
<i class="fas fa-crop-alt w-5 text-center transition-colors group-hover:text-blue-600" :class="currentTab === 'segment' ? 'text-blue-600' : 'text-slate-400'"></i>
<span class="font-medium">通用分割</span>
</a>
<a href="#" @click.prevent="switchTab('gpu')" :class="{ 'active': currentTab === 'gpu' }"
class="nav-link flex items-center gap-3 px-4 py-3 text-slate-600 hover:bg-slate-50 hover:text-blue-600 rounded-xl transition-all duration-200 group">
<i class="fas fa-microchip w-5 text-center transition-colors group-hover:text-blue-600" :class="currentTab === 'gpu' ? 'text-blue-600' : 'text-slate-400'"></i>
@@ -203,10 +221,15 @@
<!-- Main Content -->
<main class="flex-1 overflow-y-auto overflow-x-hidden relative scroll-smooth">
<!-- Header -->
<header class="sticky top-0 z-20 bg-white/80 backdrop-blur-md border-b border-slate-200 px-8 py-4 flex justify-between items-center shadow-sm">
<header class="sticky top-0 z-20 bg-white/80 backdrop-blur-md border-b border-slate-200 px-4 md:px-8 py-4 flex justify-between items-center shadow-sm transition-all">
<div class="flex items-center gap-3">
<button @click="isSidebarOpen = true" class="lg:hidden p-2 -ml-2 text-slate-500 hover:text-blue-600 hover:bg-slate-100 rounded-lg transition-colors active:scale-95">
<i class="fas fa-bars text-xl"></i>
</button>
<div>
<h1 class="text-2xl font-bold text-slate-800">{{ getPageTitle(currentTab) }}</h1>
<p class="text-sm text-slate-500 mt-0.5">{{ getPageSubtitle(currentTab) }}</p>
<h1 class="text-xl md:text-2xl font-bold text-slate-800 transition-all">{{ getPageTitle(currentTab) }}</h1>
<p class="text-xs md:text-sm text-slate-500 mt-0.5 hidden sm:block">{{ getPageSubtitle(currentTab) }}</p>
</div>
</div>
<div class="flex items-center gap-4">
<div class="bg-white border border-slate-200 rounded-full px-4 py-1.5 flex items-center gap-2 shadow-sm">
@@ -216,13 +239,13 @@
</span>
<span class="text-sm font-medium text-slate-600">系统正常</span>
</div>
<button @click="refreshData" class="p-2.5 text-slate-400 hover:text-blue-600 transition-colors rounded-full hover:bg-slate-100 active:scale-95" title="刷新数据">
<button @click="refreshData" class="p-3 text-slate-400 hover:text-blue-600 transition-colors rounded-full hover:bg-slate-100 active:scale-95" title="刷新数据">
<i class="fas fa-sync-alt" :class="{'fa-spin': isLoading}"></i>
</button>
</div>
</header>
<div class="p-8 max-w-7xl mx-auto min-h-[calc(100vh-88px)]">
<div class="p-4 md:p-8 max-w-7xl mx-auto min-h-[calc(100vh-88px)] transition-all">
<transition name="slide-up" mode="out-in">
<!-- Dashboard Tab -->
@@ -325,7 +348,7 @@
</div>
</div>
<div class="flex items-center text-xs text-slate-400">
<span class="bg-purple-100 text-purple-700 px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-wide">Qwen-VL Engine</span>
<span class="bg-purple-100 text-purple-700 px-2 py-0.5 rounded text-xs font-bold uppercase tracking-wide">Qwen-VL Engine</span>
</div>
</div>
</div>
@@ -403,7 +426,7 @@
<i class="fas fa-link text-2xl mb-2"></i>
<span class="text-xs truncate max-w-[200px]">{{ tarotImageUrl }}</span>
</div>
<button @click.prevent="clearTarotInput" class="absolute top-2 right-2 bg-white/80 p-1.5 rounded-full hover:bg-white text-slate-600 shadow-sm">
<button @click.prevent="clearTarotInput" class="absolute top-2 right-2 bg-white/80 p-2.5 rounded-full hover:bg-white text-slate-600 shadow-sm">
<i class="fas fa-times"></i>
</button>
</div>
@@ -499,7 +522,7 @@
class="text-xs font-bold px-2 py-0.5 rounded">
{{ card.recognition?.position || '未知' }}
</span>
<span v-if="card.is_rotated" class="text-[10px] text-slate-400 bg-slate-100 px-1.5 rounded" title="已自动矫正方向">
<span v-if="card.is_rotated" class="text-xs text-slate-400 bg-slate-100 px-1.5 rounded" title="已自动矫正方向">
<i class="fas fa-sync-alt"></i> Auto-Rotated
</span>
</div>
@@ -529,6 +552,216 @@
</div>
</div>
<!-- Segment Tab -->
<div v-if="currentTab === 'segment'" key="segment" class="space-y-6">
<!-- Input Section -->
<div class="bg-white rounded-2xl shadow-sm border border-slate-100 p-6">
<h3 class="font-bold text-slate-800 mb-6 flex items-center gap-2">
<span class="w-1 h-5 bg-blue-500 rounded-full"></span>
通用图像分割任务
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Image Upload -->
<div class="space-y-4">
<label class="block text-sm font-medium text-slate-700">上传图片</label>
<div class="flex items-center justify-center w-full">
<label for="dropzone-segment-file" class="flex flex-col items-center justify-center w-full h-64 border-2 border-slate-300 border-dashed rounded-xl cursor-pointer bg-slate-50 hover:bg-slate-100 transition-colors relative overflow-hidden">
<div v-if="!segmentFile && !segmentImageUrl" class="flex flex-col items-center justify-center pt-5 pb-6">
<i class="fas fa-cloud-upload-alt text-4xl text-slate-400 mb-3"></i>
<p class="mb-2 text-sm text-slate-500"><span class="font-semibold">点击上传</span> 或拖拽文件到此处</p>
<p class="text-xs text-slate-400">支持 JPG, PNG (MAX. 10MB)</p>
</div>
<div v-else class="absolute inset-0 flex items-center justify-center bg-slate-100">
<img v-if="segmentPreview" :src="segmentPreview" class="max-h-full max-w-full object-contain">
<div v-else class="text-slate-500 flex flex-col items-center">
<i class="fas fa-link text-2xl mb-2"></i>
<span class="text-xs truncate max-w-[200px]">{{ segmentImageUrl }}</span>
</div>
<button @click.prevent="clearSegmentInput" class="absolute top-2 right-2 bg-white/80 p-2.5 rounded-full hover:bg-white text-slate-600 shadow-sm">
<i class="fas fa-times"></i>
</button>
</div>
<input id="dropzone-segment-file" type="file" class="hidden" accept="image/*" @change="handleSegmentFileChange" />
</label>
</div>
<div class="relative">
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<i class="fas fa-link text-slate-400"></i>
</div>
<input type="text" v-model="segmentImageUrl" @input="handleSegmentUrlInput"
class="bg-slate-50 border border-slate-200 text-slate-900 text-sm rounded-xl focus:ring-blue-500 focus:border-blue-500 block w-full pl-10 p-2.5 outline-none"
placeholder="或者输入图片 URL...">
</div>
</div>
<!-- Settings -->
<div class="space-y-6">
<div>
<label class="block text-sm font-medium text-slate-700 mb-2">Prompt 提示词 <span class="text-red-500">*</span></label>
<input type="text" v-model="segmentPrompt"
class="bg-slate-50 border border-slate-200 text-slate-900 text-sm rounded-xl focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 outline-none"
placeholder="例如: cat, person, red car...">
<p class="text-xs text-slate-400 mt-2">支持中英文,后端会自动翻译。多个对象可用逗号分隔。</p>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-2">置信度阈值 ({{ segmentConfidence }})</label>
<input type="range" v-model.number="segmentConfidence" min="0" max="1" step="0.05" class="w-full h-2 bg-slate-200 rounded-lg appearance-none cursor-pointer accent-blue-600">
<div class="flex justify-between text-xs text-slate-400 mt-1">
<span>0.0</span>
<span>1.0</span>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<label class="flex items-center space-x-2 cursor-pointer">
<input type="checkbox" v-model="segmentOptions.save_segment_images" class="rounded text-blue-600 focus:ring-blue-500 border-gray-300">
<span class="text-sm text-slate-700">保存分割对象</span>
</label>
<label class="flex items-center space-x-2 cursor-pointer">
<input type="checkbox" v-model="segmentOptions.cutout" class="rounded text-blue-600 focus:ring-blue-500 border-gray-300">
<span class="text-sm text-slate-700">透明背景裁剪</span>
</label>
<label class="flex items-center space-x-2 cursor-pointer">
<input type="checkbox" v-model="segmentOptions.perspective_correction" class="rounded text-blue-600 focus:ring-blue-500 border-gray-300">
<span class="text-sm text-slate-700">透视矫正</span>
</label>
<label class="flex items-center space-x-2 cursor-pointer">
<input type="checkbox" v-model="segmentOptions.highlight" class="rounded text-blue-600 focus:ring-blue-500 border-gray-300">
<span class="text-sm text-slate-700">高亮主体</span>
</label>
</div>
<div class="pt-4">
<button @click="performSegment" :disabled="isSegmenting || (!segmentFile && !segmentImageUrl) || !segmentPrompt"
class="w-full bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 text-white font-bold py-3 px-4 rounded-xl shadow-lg shadow-blue-500/30 transition-all transform active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2">
<i class="fas fa-crop-alt" :class="{'fa-spin': isSegmenting}"></i>
{{ isSegmenting ? '正在分割中...' : '开始分割 (Segment)' }}
</button>
</div>
</div>
</div>
</div>
<!-- Results Section -->
<div v-if="segmentResult" class="space-y-6 animate-fade-in">
<!-- Status Banner -->
<div :class="segmentResult.status === 'success' ? 'bg-green-50 border-green-200 text-green-700' : 'bg-red-50 border-red-200 text-red-700'"
class="p-4 rounded-xl border flex items-center gap-3 shadow-sm">
<i :class="segmentResult.status === 'success' ? 'fas fa-check-circle' : 'fas fa-exclamation-circle'" class="text-xl"></i>
<div>
<h4 class="font-bold">{{ segmentResult.status === 'success' ? '分割成功' : '分割失败' }}</h4>
<p class="text-sm opacity-90">{{ segmentResult.message || (segmentResult.status === 'success' ? '图像分割任务已完成' : '发生未知错误') }}</p>
</div>
</div>
<!-- Summary Stats Card -->
<div v-if="segmentResult.status === 'success'" class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="bg-white rounded-2xl p-6 shadow-sm border border-slate-100 flex items-center gap-4">
<div class="w-12 h-12 bg-blue-50 text-blue-600 rounded-xl flex items-center justify-center text-xl">
<i class="fas fa-bullseye"></i>
</div>
<div>
<p class="text-sm text-slate-500 font-medium">检测数量 (Count)</p>
<h3 class="text-2xl font-bold text-slate-800">{{ segmentResult.detected_count || 0 }}</h3>
</div>
</div>
<div class="bg-white rounded-2xl p-6 shadow-sm border border-slate-100 flex items-center gap-4">
<div class="w-12 h-12 bg-purple-50 text-purple-600 rounded-xl flex items-center justify-center text-xl">
<i class="fas fa-chart-line"></i>
</div>
<div>
<p class="text-sm text-slate-500 font-medium">最高置信度 (Max Score)</p>
<h3 class="text-2xl font-bold text-slate-800">
{{ segmentResult.scores && segmentResult.scores.length > 0 ? (Math.max(...segmentResult.scores) * 100).toFixed(1) + '%' : '-' }}
</h3>
</div>
</div>
<div class="bg-white rounded-2xl p-6 shadow-sm border border-slate-100 flex items-center gap-4">
<div class="w-12 h-12 bg-orange-50 text-orange-600 rounded-xl flex items-center justify-center text-xl">
<i class="fas fa-layer-group"></i>
</div>
<div>
<p class="text-sm text-slate-500 font-medium">平均置信度 (Avg)</p>
<h3 class="text-2xl font-bold text-slate-800">
{{ segmentResult.scores && segmentResult.scores.length > 0 ? ((segmentResult.scores.reduce((a,b)=>a+b,0) / segmentResult.scores.length) * 100).toFixed(1) + '%' : '-' }}
</h3>
</div>
</div>
</div>
<!-- Result Visualization -->
<div v-if="segmentResult.result_image_url || segmentResult.visualization_url" class="bg-white rounded-2xl shadow-sm border border-slate-100 p-6">
<h3 class="font-bold text-slate-800 mb-4 flex items-center gap-2">
<i class="fas fa-image text-blue-500"></i>
可视化结果
</h3>
<div class="rounded-xl overflow-hidden border border-slate-200 bg-slate-50 cursor-pointer relative group"
@click="previewImage(segmentResult.result_image_url || segmentResult.visualization_url)">
<img :src="segmentResult.result_image_url || segmentResult.visualization_url" class="w-full h-auto object-contain max-h-[600px] mx-auto">
<div class="absolute inset-0 bg-black/0 group-hover:bg-black/10 transition-colors flex items-center justify-center opacity-0 group-hover:opacity-100">
<span class="bg-black/60 text-white px-4 py-2 rounded-full text-sm font-medium backdrop-blur-sm">
<i class="fas fa-search-plus mr-2"></i>点击预览大图
</span>
</div>
</div>
<div v-if="segmentResult.detected_count === 0" class="mt-4 p-4 bg-yellow-50 text-yellow-700 rounded-xl text-sm border border-yellow-100 flex items-start gap-3">
<i class="fas fa-info-circle mt-0.5"></i>
<div>
<p class="font-bold">未检测到目标</p>
<p class="opacity-90 mt-1">当前 Prompt 可能未匹配到图像中的任何物体,或者置信度阈值 ({{ segmentConfidence }}) 设置过高。建议尝试降低阈值或更换 Prompt。</p>
</div>
</div>
</div>
<!-- Scores List -->
<div v-if="segmentResult.scores && segmentResult.scores.length > 0" class="bg-white rounded-2xl shadow-sm border border-slate-100 p-6">
<h3 class="font-bold text-slate-800 mb-4 flex items-center gap-2">
<i class="fas fa-list-ol text-green-500"></i>
检测详情 (Scores)
</h3>
<div class="flex flex-wrap gap-3">
<div v-for="(score, index) in segmentResult.scores" :key="index"
class="bg-slate-50 border border-slate-200 rounded-lg px-3 py-2 flex flex-col items-center min-w-[80px]">
<span class="text-xs text-slate-400 font-mono mb-1">#{{ index + 1 }}</span>
<span class="text-lg font-bold text-slate-800 font-mono" :class="score > 0.8 ? 'text-green-600' : (score > 0.5 ? 'text-blue-600' : 'text-slate-600')">
{{ (score * 100).toFixed(1) }}%
</span>
</div>
</div>
</div>
<!-- Segmented Objects Grid (Optional) -->
<div v-if="segmentResult.segments && segmentResult.segments.length > 0" class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
<div v-for="(seg, index) in segmentResult.segments" :key="index" class="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden hover:shadow-md transition-all group">
<div class="relative aspect-square bg-slate-100 overflow-hidden cursor-pointer" @click="seg.image_url ? previewImage(seg.image_url) : null">
<img v-if="seg.image_url" :src="seg.image_url" class="w-full h-full object-contain p-2 group-hover:scale-110 transition-transform duration-500">
<div v-else class="w-full h-full flex items-center justify-center text-slate-300">
<i class="fas fa-image text-2xl"></i>
</div>
<div class="absolute top-1 right-1 bg-black/60 text-white text-[10px] font-bold px-1.5 py-0.5 rounded">
{{ (seg.score * 100).toFixed(0) }}%
</div>
</div>
<div class="p-2 text-center">
<p class="text-xs font-medium text-slate-700 truncate" :title="seg.label">{{ seg.label }}</p>
</div>
</div>
</div>
<!-- JSON Result -->
<div class="bg-white rounded-2xl shadow-sm border border-slate-100 p-6">
<h3 class="font-bold text-slate-800 mb-4 flex items-center gap-2">
<i class="fas fa-code text-slate-500"></i>
JSON 原始数据
</h3>
<pre class="bg-slate-50 text-slate-700 p-4 rounded-xl text-xs font-mono overflow-x-auto border border-slate-200 max-h-[300px]">{{ JSON.stringify(segmentResult, null, 2) }}</pre>
</div>
</div>
</div>
<!-- History Tab -->
<div v-if="currentTab === 'history'" key="history" class="bg-white rounded-2xl shadow-sm border border-slate-100 overflow-hidden">
<div class="overflow-x-auto">
@@ -561,7 +794,7 @@
</div>
<div v-if="record.final_prompt && record.final_prompt !== record.prompt" class="text-xs text-slate-500 bg-slate-50 p-1.5 rounded border border-slate-100">
<div class="flex items-center gap-1 mb-0.5 text-purple-600 font-medium">
<i class="fas fa-magic text-[10px]"></i> 优化后 Prompt
<i class="fas fa-magic text-xs"></i> 优化后 Prompt
</div>
<div class="italic line-clamp-2" :title="record.final_prompt">{{ record.final_prompt }}</div>
</div>
@@ -581,7 +814,7 @@
</td>
<td class="px-6 py-4 text-center">
<button v-if="record.result_path" @click="viewResult(record.result_path)"
class="text-blue-600 hover:text-blue-800 hover:bg-blue-50 p-2 rounded-lg transition-all" title="查看结果">
class="text-blue-600 hover:text-blue-800 hover:bg-blue-50 p-3 rounded-lg transition-all" title="查看结果">
<i class="fas fa-external-link-alt"></i>
</button>
<span v-else class="text-slate-300 cursor-not-allowed"><i class="fas fa-eye-slash"></i></span>
@@ -616,7 +849,7 @@
</button>
</div>
<div class="flex gap-2">
<button @click="fetchFiles" class="p-2 text-slate-400 hover:text-blue-600 transition-colors rounded hover:bg-slate-50">
<button @click="fetchFiles" class="p-3 text-slate-400 hover:text-blue-600 transition-colors rounded hover:bg-slate-50">
<i class="fas fa-sync-alt"></i>
</button>
</div>
@@ -630,7 +863,7 @@
<div v-if="file.is_dir" @click="enterDir(file.name)" class="flex flex-col items-center w-full h-full justify-center">
<i class="fas fa-folder text-yellow-400 text-5xl mb-2 drop-shadow-sm group-hover:scale-110 transition-transform"></i>
<span class="text-xs font-medium text-slate-700 truncate w-full text-center px-2">{{ file.name }}</span>
<span class="text-[10px] text-slate-400">{{ file.count }} 项</span>
<span class="text-xs text-slate-400">{{ file.count }} 项</span>
</div>
<!-- Image -->
@@ -639,19 +872,19 @@
<img :src="file.url" class="w-full h-full object-contain" loading="lazy">
</div>
<span class="text-xs font-medium text-slate-700 truncate w-full text-center px-1">{{ file.name }}</span>
<span class="text-[10px] text-slate-400 font-mono mt-0.5 bg-slate-100 px-1.5 rounded">{{ formatBytes(file.size) }}</span>
<span class="text-xs text-slate-400 font-mono mt-0.5 bg-slate-100 px-1.5 rounded">{{ formatBytes(file.size) }}</span>
</div>
<!-- Other -->
<div v-else class="flex flex-col items-center w-full h-full justify-center">
<i class="fas fa-file-alt text-slate-300 text-4xl mb-2"></i>
<span class="text-xs font-medium text-slate-700 truncate w-full text-center px-1">{{ file.name }}</span>
<span class="text-[10px] text-slate-400 font-mono mt-0.5 bg-slate-100 px-1.5 rounded">{{ formatBytes(file.size) }}</span>
<span class="text-xs text-slate-400 font-mono mt-0.5 bg-slate-100 px-1.5 rounded">{{ formatBytes(file.size) }}</span>
</div>
<!-- Delete Action -->
<button @click.stop="deleteFile(file.name)"
class="absolute top-2 right-2 w-6 h-6 bg-red-500 text-white rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-all transform scale-90 group-hover:scale-100 hover:bg-red-600 shadow-sm z-10">
class="absolute top-2 right-2 w-8 h-8 bg-red-500 text-white rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-all transform scale-90 group-hover:scale-100 hover:bg-red-600 shadow-sm z-10">
<i class="fas fa-times text-xs"></i>
</button>
</div>
@@ -927,6 +1160,7 @@
setup() {
// State
const isLoggedIn = ref(false);
const isSidebarOpen = ref(false);
const password = ref('');
const loginError = ref('');
const currentTab = ref('dashboard');
@@ -953,6 +1187,21 @@
const tarotResult = ref(null);
const isRecognizing = ref(false);
// Segment State
const segmentFile = ref(null);
const segmentImageUrl = ref('');
const segmentPreview = ref(null);
const segmentPrompt = ref('');
const segmentConfidence = ref(0.7);
const segmentOptions = ref({
save_segment_images: false,
cutout: false,
perspective_correction: false,
highlight: false
});
const segmentResult = ref(null);
const isSegmenting = ref(false);
// Filters
const selectedTimeRange = ref('all');
const selectedType = ref('all');
@@ -1360,17 +1609,11 @@
}
formData.append('expected_count', tarotExpectedCount.value);
// Use axios directly or a helper. Need to handle API Key if required by backend,
// but admin usually has session. Wait, the backend endpoints like /recognize_tarot
// require X-API-Key header.
// The admin page uses cookie for /admin/api/* but /recognize_tarot is a public API protected by Key.
// We should add the key to the header.
const config = {
headers: {
'X-API-Key': '123quant-speed' // Hardcoded as per fastAPI_tarot.py VALID_API_KEY
'X-API-Key': '123quant-speed'
},
timeout: 120000 // 2分钟超时大模型响应较慢
timeout: 120000
};
const res = await axios.post('/recognize_tarot', formData, config);
@@ -1380,7 +1623,6 @@
console.error(e);
let msg = e.response?.data?.detail || e.message || '识别请求失败';
// 针对 504 Gateway Timeout 或 请求超时做特殊提示
if (e.response && e.response.status === 504) {
msg = '请求超时 (504):大模型处理时间较长。后台可能仍在运行,请稍后在“识别记录”中刷新查看结果。';
} else if (e.code === 'ECONNABORTED') {
@@ -1396,10 +1638,90 @@
}
};
// --- Segment Actions ---
const handleSegmentFileChange = (event) => {
const file = event.target.files[0];
if (file) {
segmentFile.value = file;
segmentImageUrl.value = '';
segmentPreview.value = URL.createObjectURL(file);
segmentResult.value = null;
}
};
const handleSegmentUrlInput = () => {
if (segmentImageUrl.value) {
segmentFile.value = null;
segmentPreview.value = segmentImageUrl.value;
segmentResult.value = null;
} else {
segmentPreview.value = null;
}
};
const clearSegmentInput = () => {
segmentFile.value = null;
segmentImageUrl.value = '';
segmentPreview.value = null;
segmentResult.value = null;
const fileInput = document.getElementById('dropzone-segment-file');
if (fileInput) fileInput.value = '';
};
const performSegment = async () => {
if ((!segmentFile.value && !segmentImageUrl.value) || !segmentPrompt.value) return;
isSegmenting.value = true;
segmentResult.value = null;
try {
const formData = new FormData();
if (segmentFile.value) {
formData.append('file', segmentFile.value);
} else {
formData.append('image_url', segmentImageUrl.value);
}
formData.append('prompt', segmentPrompt.value);
formData.append('confidence', segmentConfidence.value);
formData.append('save_segment_images', segmentOptions.value.save_segment_images);
formData.append('cutout', segmentOptions.value.cutout);
formData.append('perspective_correction', segmentOptions.value.perspective_correction);
formData.append('highlight', segmentOptions.value.highlight);
const config = {
headers: {
'X-API-Key': '123quant-speed'
},
timeout: 120000
};
const res = await axios.post('/segment', formData, config);
segmentResult.value = res.data;
} catch (e) {
console.error(e);
let msg = e.response?.data?.detail || e.message || '分割请求失败';
if (e.response && e.response.status === 504) {
msg = '请求超时 (504):处理时间较长。';
} else if (e.code === 'ECONNABORTED') {
msg = '请求超时:网络连接中断或服务器响应过慢。';
}
segmentResult.value = {
status: 'failed',
message: msg
};
} finally {
isSegmenting.value = false;
}
};
// --- Navigation & Helpers ---
const switchTab = (tab) => {
const prevTab = currentTab.value;
currentTab.value = tab;
isSidebarOpen.value = false; // Close sidebar on mobile
// GPU Monitor Logic
if (tab === 'gpu') {
@@ -1486,6 +1808,7 @@
const map = {
'dashboard': '数据看板',
'tarot': '塔罗牌识别',
'segment': '通用分割',
'history': '识别记录',
'files': '文件资源管理',
'prompts': '提示词工程',
@@ -1499,6 +1822,7 @@
const map = {
'dashboard': '系统运行状态与核心指标概览',
'tarot': 'SAM3 + Qwen-VL 联合识别与分割',
'segment': '基于文本提示的通用图像分割 (Grounded SAM)',
'history': '所有视觉识别任务的历史流水',
'files': '查看和管理生成的图像及JSON结果',
'prompts': '调整各个识别场景的 System Prompt',
@@ -1696,7 +2020,7 @@
});
return {
isLoggedIn, password, loginError, login, logout,
isLoggedIn, isSidebarOpen, password, loginError, login, logout,
currentTab, switchTab, history, files, currentPath,
enterDir, navigateUp, deleteFile, triggerCleanup,
viewResult, previewImage, isImage, previewUrl,
@@ -1714,7 +2038,10 @@
gpuUtilChartRef, gpuTempChartRef,
// Tarot
tarotFile, tarotImageUrl, tarotExpectedCount, tarotPreview, tarotResult, isRecognizing,
handleTarotFileChange, handleUrlInput, clearTarotInput, recognizeTarot
handleTarotFileChange, handleUrlInput, clearTarotInput, recognizeTarot,
// Segment
segmentFile, segmentImageUrl, segmentPreview, segmentPrompt, segmentConfidence, segmentOptions, segmentResult, isSegmenting,
handleSegmentFileChange, handleSegmentUrlInput, clearSegmentInput, performSegment
};
}
}).mount('#app');