|
|
|
|
@ -8,7 +8,7 @@
|
|
|
|
|
<div class="kt-preview-header">
|
|
|
|
|
<div class="kt-preview-header-info">
|
|
|
|
|
<h3>结果预览</h3>
|
|
|
|
|
<span class="kt-preview-task-tag">Task #{{ taskId }}</span>
|
|
|
|
|
<span class="kt-preview-task-tag">Task #{{ virtualId || taskId }}</span>
|
|
|
|
|
<span v-if="isFinetuneTask" class="kt-preview-mode-tag">
|
|
|
|
|
{{ isUploadFinetune ? '上传源微调 (Upload)' : '加噪源微调 (Perturbation)' }}
|
|
|
|
|
</span>
|
|
|
|
|
@ -49,10 +49,9 @@
|
|
|
|
|
|
|
|
|
|
<!--
|
|
|
|
|
场景1: 单栏长图报告模式
|
|
|
|
|
适用:评估任务 (evaluate/metrics) 和 热力图任务 (heatmap)
|
|
|
|
|
-->
|
|
|
|
|
<div v-else-if="isEvaluateTask" class="kt-preview-report-stage">
|
|
|
|
|
<div class="kt-preview-report-container allow-scroll">
|
|
|
|
|
<div class="kt-preview-report-container">
|
|
|
|
|
<div class="kt-preview-report-header-tip">
|
|
|
|
|
<i class="fas fa-file-invoice"></i>
|
|
|
|
|
{{ taskType === 'heatmap' ? '热力图报告 (Heatmap Report)' : '评估报告 (Evaluation Report)' }}
|
|
|
|
|
@ -70,7 +69,6 @@
|
|
|
|
|
<div class="kt-preview-img-box kt-preview-original">
|
|
|
|
|
<span class="kt-preview-img-label">{{ leftImageLabel }}</span>
|
|
|
|
|
|
|
|
|
|
<!-- 页码指示器 (仅微调任务显示) -->
|
|
|
|
|
<span v-if="isFinetuneTask && finetuneLeftList.length > 0" class="kt-preview-page-indicator">
|
|
|
|
|
{{ leftIndex + 1 }} / {{ finetuneLeftList.length }}
|
|
|
|
|
</span>
|
|
|
|
|
@ -78,13 +76,12 @@
|
|
|
|
|
<img :src="currentOriginalSrc" alt="Left Image" v-if="currentOriginalSrc" />
|
|
|
|
|
<div v-else class="kt-preview-no-img">
|
|
|
|
|
<span v-if="isFinetuneTask">
|
|
|
|
|
<i class="fas fa-robot" style="display:block; font-size: var(--cq-font-2xl); margin-bottom: 1cqh; color: var(--kt-muted-fg);"></i>
|
|
|
|
|
<i class="fas fa-robot" style="display:block; font-size: 2rem; margin-bottom: 1rem; color: var(--kt-muted-fg);"></i>
|
|
|
|
|
生成中或无数据<br>(Waiting for Generation)
|
|
|
|
|
</span>
|
|
|
|
|
<span v-else>无图片数据</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 独立翻页控件 -->
|
|
|
|
|
<div v-if="isFinetuneTask && finetuneLeftList.length > 1" class="kt-preview-nav-controls">
|
|
|
|
|
<button class="kt-preview-nav-arrow left" @click.stop="changeLeft(-1)" :disabled="leftIndex === 0">
|
|
|
|
|
<i class="fas fa-chevron-left"></i>
|
|
|
|
|
@ -105,7 +102,6 @@
|
|
|
|
|
<div class="kt-preview-img-box kt-preview-result">
|
|
|
|
|
<span class="kt-preview-img-label kt-preview-result-label">{{ rightImageLabel }}</span>
|
|
|
|
|
|
|
|
|
|
<!-- 页码指示器 -->
|
|
|
|
|
<span v-if="isFinetuneTask && finetuneRightList.length > 0" class="kt-preview-page-indicator">
|
|
|
|
|
{{ rightIndex + 1 }} / {{ finetuneRightList.length }}
|
|
|
|
|
</span>
|
|
|
|
|
@ -113,13 +109,12 @@
|
|
|
|
|
<img :src="currentResultSrc" alt="Result" v-if="currentResultSrc" />
|
|
|
|
|
<div v-else class="kt-preview-no-img">
|
|
|
|
|
<span v-if="isFinetuneTask">
|
|
|
|
|
<i class="fas fa-hourglass-half" style="display:block; font-size: var(--cq-font-2xl); margin-bottom: 1cqh; color: var(--kt-muted-fg);"></i>
|
|
|
|
|
<i class="fas fa-hourglass-half" style="display:block; font-size: 2rem; margin-bottom: 1rem; color: var(--kt-muted-fg);"></i>
|
|
|
|
|
暂无生成结果<br>(Pending / No Data)
|
|
|
|
|
</span>
|
|
|
|
|
<span v-else>暂无结果</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 独立翻页控件 -->
|
|
|
|
|
<div v-if="isFinetuneTask && finetuneRightList.length > 1" class="kt-preview-nav-controls">
|
|
|
|
|
<button class="kt-preview-nav-arrow left" @click.stop="changeRight(-1)" :disabled="rightIndex === 0">
|
|
|
|
|
<i class="fas fa-chevron-left"></i>
|
|
|
|
|
@ -133,7 +128,7 @@
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 底部数字翻页 (仅非微调且非报告任务显示) -->
|
|
|
|
|
<!-- 底部数字翻页 -->
|
|
|
|
|
<div class="kt-preview-footer" v-if="totalPairs > 1 && !isFinetuneTask && !isEvaluateTask">
|
|
|
|
|
<div class="kt-preview-thumb-list">
|
|
|
|
|
<div
|
|
|
|
|
@ -161,13 +156,14 @@
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
import { ref, computed, watch } from 'vue'
|
|
|
|
|
import { getTaskImagePreview } from '@/api/image'
|
|
|
|
|
import { ref, computed, watch, onUnmounted } from 'vue'
|
|
|
|
|
import { getTaskImages } from '@/api/image'
|
|
|
|
|
import ThreeDTrajectoryModal from '@/components/ThreeDTrajectoryModal.vue'
|
|
|
|
|
|
|
|
|
|
const props = defineProps({
|
|
|
|
|
isOpen: Boolean,
|
|
|
|
|
taskId: [String, Number],
|
|
|
|
|
virtualId: [String, Number],
|
|
|
|
|
taskType: { type: String, default: '' }
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
@ -178,10 +174,9 @@ const error = ref(null)
|
|
|
|
|
const previewData = ref(null)
|
|
|
|
|
const show3DModal = ref(false)
|
|
|
|
|
|
|
|
|
|
// 状态管理
|
|
|
|
|
const currentIndex = ref(0) // 全局同步索引
|
|
|
|
|
const leftIndex = ref(0) // 左侧独立索引
|
|
|
|
|
const rightIndex = ref(0) // 右侧独立索引
|
|
|
|
|
const currentIndex = ref(0)
|
|
|
|
|
const leftIndex = ref(0)
|
|
|
|
|
const rightIndex = ref(0)
|
|
|
|
|
|
|
|
|
|
const isFinetuneTask = computed(() => props.taskType === 'finetune')
|
|
|
|
|
|
|
|
|
|
@ -189,7 +184,6 @@ const isEvaluateTask = computed(() => {
|
|
|
|
|
return ['evaluate', 'metrics', 'heatmap'].includes(props.taskType)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// === 核心判断:是否为上传源的微调任务 ===
|
|
|
|
|
const isUploadFinetune = computed(() => {
|
|
|
|
|
if (!isFinetuneTask.value || !previewData.value?.images) return false
|
|
|
|
|
if (previewData.value.images.uploaded_generate?.length > 0) return true
|
|
|
|
|
@ -198,7 +192,6 @@ const isUploadFinetune = computed(() => {
|
|
|
|
|
return false
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// === 核心数据源拆分 (微调任务专用) ===
|
|
|
|
|
const finetuneLeftList = computed(() => {
|
|
|
|
|
const imgs = previewData.value?.images
|
|
|
|
|
if (!imgs || !isFinetuneTask.value) return []
|
|
|
|
|
@ -219,7 +212,6 @@ const finetuneRightList = computed(() => {
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// === 独立翻页动作 ===
|
|
|
|
|
const changeLeft = (delta) => {
|
|
|
|
|
const newIndex = leftIndex.value + delta
|
|
|
|
|
if (newIndex >= 0 && newIndex < finetuneLeftList.value.length) leftIndex.value = newIndex
|
|
|
|
|
@ -232,22 +224,6 @@ const changeRight = (delta) => {
|
|
|
|
|
|
|
|
|
|
const open3DGraph = () => { show3DModal.value = true }
|
|
|
|
|
|
|
|
|
|
// 数据加载与重置
|
|
|
|
|
watch(() => props.isOpen, (val) => {
|
|
|
|
|
if (val && props.taskId) {
|
|
|
|
|
fetchData()
|
|
|
|
|
} else {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
previewData.value = null
|
|
|
|
|
currentIndex.value = 0
|
|
|
|
|
leftIndex.value = 0
|
|
|
|
|
rightIndex.value = 0
|
|
|
|
|
error.value = null
|
|
|
|
|
show3DModal.value = false
|
|
|
|
|
}, 300)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const clearBlobs = () => {
|
|
|
|
|
if (previewData.value && previewData.value.images) {
|
|
|
|
|
Object.values(previewData.value.images).forEach(list => {
|
|
|
|
|
@ -260,6 +236,18 @@ const clearBlobs = () => {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 背景滑动问题:切换 body 的 overflow 状态
|
|
|
|
|
const toggleBodyScroll = (lock) => {
|
|
|
|
|
if (typeof document !== 'undefined' && document.body) {
|
|
|
|
|
document.body.style.overflow = lock ? 'hidden' : ''
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
clearBlobs()
|
|
|
|
|
toggleBodyScroll(false)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const fetchData = async () => {
|
|
|
|
|
loading.value = true
|
|
|
|
|
error.value = null
|
|
|
|
|
@ -267,7 +255,7 @@ const fetchData = async () => {
|
|
|
|
|
previewData.value = null
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const res = await getTaskImagePreview(props.taskId)
|
|
|
|
|
const res = await getTaskImages(props.taskId)
|
|
|
|
|
if (!res || !res.images) throw new Error("无图片数据")
|
|
|
|
|
previewData.value = res
|
|
|
|
|
} catch (err) {
|
|
|
|
|
@ -278,19 +266,25 @@ const fetchData = async () => {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 在组件销毁或关闭时清理
|
|
|
|
|
watch(() => props.isOpen, (val) => {
|
|
|
|
|
// 控制背景滚动
|
|
|
|
|
toggleBodyScroll(val)
|
|
|
|
|
|
|
|
|
|
if (val && props.taskId) {
|
|
|
|
|
fetchData()
|
|
|
|
|
} else {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
clearBlobs()
|
|
|
|
|
previewData.value = null
|
|
|
|
|
currentIndex.value = 0
|
|
|
|
|
leftIndex.value = 0
|
|
|
|
|
rightIndex.value = 0
|
|
|
|
|
error.value = null
|
|
|
|
|
show3DModal.value = false
|
|
|
|
|
}, 300)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// === 1. Total Pairs ===
|
|
|
|
|
const totalPairs = computed(() => {
|
|
|
|
|
if (isFinetuneTask.value) return 0
|
|
|
|
|
if (!previewData.value || !previewData.value.images) return 0
|
|
|
|
|
@ -303,80 +297,61 @@ const totalPairs = computed(() => {
|
|
|
|
|
return 0
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// === 2. 左侧图片源 (URL) ===
|
|
|
|
|
const currentOriginalSrc = computed(() => {
|
|
|
|
|
const imgs = previewData.value?.images
|
|
|
|
|
if (!imgs) return null
|
|
|
|
|
|
|
|
|
|
if (isFinetuneTask.value) {
|
|
|
|
|
const list = finetuneLeftList.value
|
|
|
|
|
if (list && list[leftIndex.value]) return list[leftIndex.value].data
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const idx = currentIndex.value
|
|
|
|
|
if (imgs.original && imgs.original[idx]) return imgs.original[idx].data
|
|
|
|
|
if (imgs.original_generate && imgs.original_generate[idx]) return imgs.original_generate[idx].data
|
|
|
|
|
|
|
|
|
|
return null
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// === 3. 右侧图片源 / 报告图源 (URL) ===
|
|
|
|
|
const currentResultSrc = computed(() => {
|
|
|
|
|
const imgs = previewData.value?.images
|
|
|
|
|
if (!imgs) return null
|
|
|
|
|
|
|
|
|
|
if (isFinetuneTask.value) {
|
|
|
|
|
const list = finetuneRightList.value
|
|
|
|
|
if (list && list[rightIndex.value]) return list[rightIndex.value].data
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const idx = currentIndex.value
|
|
|
|
|
if (imgs.heatmap && imgs.heatmap[idx]) return imgs.heatmap[idx].data
|
|
|
|
|
if (imgs.report && imgs.report[idx]) return imgs.report[idx].data
|
|
|
|
|
if (imgs.perturbed && imgs.perturbed[idx]) return imgs.perturbed[idx].data
|
|
|
|
|
if (imgs.perturbed_generate && imgs.perturbed_generate[idx]) return imgs.perturbed_generate[idx].data
|
|
|
|
|
if (imgs.uploaded_generate && imgs.uploaded_generate[idx]) return imgs.uploaded_generate[idx].data
|
|
|
|
|
|
|
|
|
|
return null
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 4. 左侧标签
|
|
|
|
|
const leftImageLabel = computed(() => {
|
|
|
|
|
const imgs = previewData.value?.images
|
|
|
|
|
if (!imgs) return 'Original'
|
|
|
|
|
|
|
|
|
|
if (isFinetuneTask.value) {
|
|
|
|
|
if (isUploadFinetune.value) return '上传原图 (Uploaded Input)'
|
|
|
|
|
return '未防护微调结果 (Clean Gen)'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (imgs.original) return 'Original (原始)'
|
|
|
|
|
if (imgs.perturbed_generate) return 'Protected Gen (防护后生成)'
|
|
|
|
|
|
|
|
|
|
return 'Reference'
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 5. 右侧标签
|
|
|
|
|
const rightImageLabel = computed(() => {
|
|
|
|
|
const imgs = previewData.value?.images
|
|
|
|
|
if (!imgs) return 'Result'
|
|
|
|
|
|
|
|
|
|
if (isFinetuneTask.value) {
|
|
|
|
|
if (isUploadFinetune.value) return '微调生成 (Finetuned)'
|
|
|
|
|
return '防护后微调结果 (Protected Gen)'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 优先判断是否为加噪任务的加噪图片
|
|
|
|
|
if (imgs.perturbed && imgs.perturbed.length > 0) return 'Perturbed (加噪图片)'
|
|
|
|
|
|
|
|
|
|
// 判断是否为微调生成图 (且长度大于0)
|
|
|
|
|
if ((imgs.uploaded_generate && imgs.uploaded_generate.length > 0) ||
|
|
|
|
|
(imgs.perturbed_generate && imgs.perturbed_generate.length > 0)) {
|
|
|
|
|
return 'Finetuned (微调生成)'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 'Protected (防护后)'
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
@ -389,39 +364,62 @@ const close = () => { emit('close') }
|
|
|
|
|
.kt-preview-overlay {
|
|
|
|
|
position: fixed; inset: 0;
|
|
|
|
|
background: rgba(0, 0, 0, 0.6);
|
|
|
|
|
backdrop-filter: blur(0.8cqw);
|
|
|
|
|
backdrop-filter: blur(8px);
|
|
|
|
|
z-index: 2000;
|
|
|
|
|
display: flex; justify-content: center; align-items: center;
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
/* align-items: center; */
|
|
|
|
|
padding: 4vh 2vw; /* 增加上下边距 */
|
|
|
|
|
overflow-y: auto; /* 允许 Overlay 本身滚动,以防 Card 高度超屏幕 */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.kt-preview-card {
|
|
|
|
|
width: 95cqw; max-width: 140cqw; height: 85cqh;
|
|
|
|
|
min-width: 30cqw;
|
|
|
|
|
min-height: 40cqh;
|
|
|
|
|
/* 使用 margin: auto 实现 Flexbox 下的安全居中 (溢出时自动顶部对齐) */
|
|
|
|
|
margin: auto;
|
|
|
|
|
|
|
|
|
|
width: 90vw;
|
|
|
|
|
max-width: 1400px;
|
|
|
|
|
|
|
|
|
|
/* 使用 vh 限制高度,确保不会被撑爆 */
|
|
|
|
|
height: 85vh;
|
|
|
|
|
max-height: 900px;
|
|
|
|
|
|
|
|
|
|
min-width: 320px;
|
|
|
|
|
min-height: 400px;
|
|
|
|
|
|
|
|
|
|
background: var(--kt-bg);
|
|
|
|
|
border: 2px solid var(--kt-border);
|
|
|
|
|
border-radius: var(--kt-radius);
|
|
|
|
|
display: flex; flex-direction: column;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
box-shadow: 0 20px 50px rgba(0,0,0,0.3);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@media (max-width: 900px) {
|
|
|
|
|
.kt-preview-card {
|
|
|
|
|
width: 95cqw; height: 90cqh;
|
|
|
|
|
width: 95vw;
|
|
|
|
|
height: 90vh;
|
|
|
|
|
max-height: 90vh;
|
|
|
|
|
}
|
|
|
|
|
.kt-preview-image-stage {
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
padding: 1cqw;
|
|
|
|
|
padding: 1rem;
|
|
|
|
|
height: auto; /* Allow grow */
|
|
|
|
|
}
|
|
|
|
|
.kt-preview-img-box {
|
|
|
|
|
width: 100%; height: 35cqh; flex: none; margin-bottom: 1cqw;
|
|
|
|
|
width: 100%;
|
|
|
|
|
/* 移动端限制图片框高度 */
|
|
|
|
|
height: 35vh;
|
|
|
|
|
min-height: 200px;
|
|
|
|
|
flex: none;
|
|
|
|
|
margin-bottom: 1rem;
|
|
|
|
|
}
|
|
|
|
|
.kt-preview-divider { transform: rotate(90deg); margin: 1cqw 0; }
|
|
|
|
|
.kt-preview-divider { transform: rotate(90deg); margin: 1rem 0; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.kt-preview-header {
|
|
|
|
|
padding: 1.5cqw 2.5cqw;
|
|
|
|
|
padding: 1.5rem 2rem;
|
|
|
|
|
border-bottom: 2px solid var(--kt-border);
|
|
|
|
|
display: flex; justify-content: space-between; align-items: center;
|
|
|
|
|
background: var(--kt-bg);
|
|
|
|
|
@ -432,7 +430,7 @@ const close = () => { emit('close') }
|
|
|
|
|
margin: 0;
|
|
|
|
|
font-family: var(--kt-font);
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
font-size: var(--cq-font-lg);
|
|
|
|
|
font-size: 1.5rem;
|
|
|
|
|
color: var(--kt-fg);
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: -0.02em;
|
|
|
|
|
@ -440,37 +438,37 @@ const close = () => { emit('close') }
|
|
|
|
|
|
|
|
|
|
.kt-preview-task-tag {
|
|
|
|
|
font-family: var(--kt-font);
|
|
|
|
|
font-size: var(--cq-font-xs);
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
background: var(--kt-muted);
|
|
|
|
|
padding: 0.2cqw 0.8cqw;
|
|
|
|
|
padding: 0.2rem 0.6rem;
|
|
|
|
|
border-radius: var(--kt-radius);
|
|
|
|
|
border: 1px solid var(--kt-border);
|
|
|
|
|
color: var(--kt-fg);
|
|
|
|
|
margin-top: 0.4cqw;
|
|
|
|
|
margin-top: 0.4rem;
|
|
|
|
|
display: inline-block;
|
|
|
|
|
margin-right: 0.5cqw;
|
|
|
|
|
margin-right: 0.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.kt-preview-mode-tag {
|
|
|
|
|
font-family: var(--kt-font);
|
|
|
|
|
font-size: 0.75cqw;
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
background: var(--kt-accent);
|
|
|
|
|
color: var(--kt-accent-fg);
|
|
|
|
|
padding: 0.2cqw 0.8cqw;
|
|
|
|
|
padding: 0.2rem 0.6rem;
|
|
|
|
|
border-radius: var(--kt-radius);
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.kt-preview-header-actions { display: flex; align-items: center; gap: 1.5cqw; }
|
|
|
|
|
.kt-preview-header-actions { display: flex; align-items: center; gap: 1rem; }
|
|
|
|
|
|
|
|
|
|
.kt-preview-close-btn {
|
|
|
|
|
background: var(--kt-bg);
|
|
|
|
|
border: 2px solid var(--kt-border);
|
|
|
|
|
border-radius: var(--kt-radius);
|
|
|
|
|
font-size: var(--cq-font-xl);
|
|
|
|
|
font-size: 1.25rem;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
color: var(--kt-fg);
|
|
|
|
|
padding: 0.4cqw 0.8cqw;
|
|
|
|
|
padding: 0.4rem 0.8rem;
|
|
|
|
|
transition: all var(--kt-transition-micro);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -480,16 +478,21 @@ const close = () => { emit('close') }
|
|
|
|
|
border-color: var(--kt-accent);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Body 负责内部滚动 */
|
|
|
|
|
.kt-preview-body {
|
|
|
|
|
flex: 1;
|
|
|
|
|
position: relative;
|
|
|
|
|
background: var(--kt-bg);
|
|
|
|
|
display: flex; justify-content: center; align-items: center;
|
|
|
|
|
/* block 布局让 padding 生效 */
|
|
|
|
|
display: block;
|
|
|
|
|
padding: 0;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
overflow-y: auto; /* 让 Body 内部滚动 */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.kt-preview-loading, .kt-preview-error {
|
|
|
|
|
/* 居中 loading/error */
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 50%; left: 50%; transform: translate(-50%, -50%);
|
|
|
|
|
width: 100%;
|
|
|
|
|
text-align: center;
|
|
|
|
|
color: var(--kt-fg);
|
|
|
|
|
@ -497,22 +500,31 @@ const close = () => { emit('close') }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.kt-preview-error { color: var(--kt-accent); }
|
|
|
|
|
.kt-preview-loading i { color: var(--kt-accent); margin-bottom: 2cqw; }
|
|
|
|
|
.kt-preview-loading i { color: var(--kt-accent); margin-bottom: 1rem; }
|
|
|
|
|
|
|
|
|
|
/* === 图片展示区 === */
|
|
|
|
|
.kt-preview-image-stage {
|
|
|
|
|
display: flex; align-items: center; justify-content: center;
|
|
|
|
|
width: 100%; height: 100%; gap: 2cqw;
|
|
|
|
|
padding: 2cqw;
|
|
|
|
|
display: flex;
|
|
|
|
|
/* 移除 align-items: center,防止高度溢出时被居中裁剪 */
|
|
|
|
|
align-items: stretch;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
width: 100%;
|
|
|
|
|
/* 使用 min-height 撑满,允许内容撑开 */
|
|
|
|
|
min-height: 100%;
|
|
|
|
|
gap: 2vw;
|
|
|
|
|
padding: 2rem; /* 给足内边距,防止贴边 */
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.kt-preview-img-box {
|
|
|
|
|
flex: 1; height: 100%;
|
|
|
|
|
flex: 1;
|
|
|
|
|
/* 移除固定高度,让它自适应 */
|
|
|
|
|
min-height: 300px;
|
|
|
|
|
background: var(--kt-bg);
|
|
|
|
|
border-radius: var(--kt-radius);
|
|
|
|
|
display: flex; flex-direction: column; position: relative;
|
|
|
|
|
border: 2px solid var(--kt-border);
|
|
|
|
|
padding: 1cqw;
|
|
|
|
|
padding: 1rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 导航箭头 */
|
|
|
|
|
@ -522,13 +534,13 @@ const close = () => { emit('close') }
|
|
|
|
|
display: flex; justify-content: space-between;
|
|
|
|
|
transform: translateY(-50%);
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
padding: 0 1cqw;
|
|
|
|
|
padding: 0 1rem;
|
|
|
|
|
z-index: 10;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.kt-preview-nav-arrow {
|
|
|
|
|
pointer-events: auto;
|
|
|
|
|
width: 4cqw; height: 4cqw;
|
|
|
|
|
width: 3rem; height: 3rem;
|
|
|
|
|
border-radius: var(--kt-radius);
|
|
|
|
|
border: 2px solid var(--kt-border);
|
|
|
|
|
background: var(--kt-bg);
|
|
|
|
|
@ -548,11 +560,11 @@ const close = () => { emit('close') }
|
|
|
|
|
.kt-preview-nav-arrow:disabled { opacity: 0.3; cursor: not-allowed; }
|
|
|
|
|
|
|
|
|
|
.kt-preview-img-label {
|
|
|
|
|
position: absolute; top: 1.5cqw; left: 1.5cqw;
|
|
|
|
|
position: absolute; top: 1rem; left: 1rem;
|
|
|
|
|
background: var(--kt-fg); color: var(--kt-bg);
|
|
|
|
|
padding: 0.4cqw 1.2cqw;
|
|
|
|
|
padding: 0.4rem 1rem;
|
|
|
|
|
border-radius: var(--kt-radius);
|
|
|
|
|
font-size: var(--cq-font-xs);
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
font-family: var(--kt-font);
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
z-index: 2;
|
|
|
|
|
@ -563,11 +575,11 @@ const close = () => { emit('close') }
|
|
|
|
|
.kt-preview-result-label { background: var(--kt-accent); color: var(--kt-accent-fg); }
|
|
|
|
|
|
|
|
|
|
.kt-preview-page-indicator {
|
|
|
|
|
position: absolute; top: 1.5cqw; right: 1.5cqw;
|
|
|
|
|
position: absolute; top: 1rem; right: 1rem;
|
|
|
|
|
background: var(--kt-muted); color: var(--kt-fg);
|
|
|
|
|
padding: 0.4cqw 1cqw;
|
|
|
|
|
padding: 0.4rem 1rem;
|
|
|
|
|
border-radius: var(--kt-radius);
|
|
|
|
|
font-size: var(--cq-font-xs);
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
font-family: var(--kt-font);
|
|
|
|
|
z-index: 2;
|
|
|
|
|
border: 1px solid var(--kt-border);
|
|
|
|
|
@ -577,15 +589,19 @@ const close = () => { emit('close') }
|
|
|
|
|
width: 100%; height: 100%; object-fit: contain; border-radius: var(--kt-radius);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.kt-preview-divider { color: var(--kt-fg); font-size: var(--cq-font-2xl); opacity: 0.5; }
|
|
|
|
|
.kt-preview-spacer { width: 1cqw; }
|
|
|
|
|
.kt-preview-divider {
|
|
|
|
|
color: var(--kt-fg); font-size: 2rem; opacity: 0.5;
|
|
|
|
|
/* 垂直居中图标 */
|
|
|
|
|
align-self: center;
|
|
|
|
|
}
|
|
|
|
|
.kt-preview-spacer { width: 1vw; }
|
|
|
|
|
|
|
|
|
|
.kt-preview-no-img {
|
|
|
|
|
width: 100%; height: 100%;
|
|
|
|
|
display: flex; align-items: center; justify-content: center;
|
|
|
|
|
color: var(--kt-muted-fg);
|
|
|
|
|
text-align: center;
|
|
|
|
|
font-size: var(--cq-font-sm);
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
font-family: var(--kt-font);
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
@ -593,28 +609,31 @@ const close = () => { emit('close') }
|
|
|
|
|
|
|
|
|
|
/* === 报告视图 (热力图/评估报告) === */
|
|
|
|
|
.kt-preview-report-stage {
|
|
|
|
|
width: 100%; height: 100%;
|
|
|
|
|
width: 100%; min-height: 100%;
|
|
|
|
|
background: var(--kt-bg);
|
|
|
|
|
display: flex; justify-content: center;
|
|
|
|
|
padding: 2rem;
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.kt-preview-report-container {
|
|
|
|
|
width: 100%; max-width: 90cqw; height: 100%;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
width: 100%; max-width: 90vw;
|
|
|
|
|
/* 移除固定 height: 100%,让其自适应内容 */
|
|
|
|
|
overflow-y: visible;
|
|
|
|
|
background: var(--kt-bg);
|
|
|
|
|
border: 2px solid var(--kt-border);
|
|
|
|
|
border-radius: var(--kt-radius);
|
|
|
|
|
padding: 2cqw;
|
|
|
|
|
padding: 2rem;
|
|
|
|
|
text-align: center;
|
|
|
|
|
margin: 1cqw;
|
|
|
|
|
/* margin: 1rem; */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.kt-preview-report-header-tip {
|
|
|
|
|
color: var(--kt-fg);
|
|
|
|
|
font-size: var(--cq-font-sm);
|
|
|
|
|
font-size: 1rem;
|
|
|
|
|
font-family: var(--kt-font);
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
margin-bottom: 2cqw; padding: 1cqw 1.5cqw;
|
|
|
|
|
margin-bottom: 2rem; padding: 1rem 1.5rem;
|
|
|
|
|
background: var(--kt-muted);
|
|
|
|
|
border-radius: var(--kt-radius);
|
|
|
|
|
display: inline-block;
|
|
|
|
|
@ -624,10 +643,10 @@ const close = () => { emit('close') }
|
|
|
|
|
|
|
|
|
|
.kt-preview-sub-tip {
|
|
|
|
|
display: block;
|
|
|
|
|
font-size: var(--cq-font-xs);
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
font-weight: 400;
|
|
|
|
|
color: var(--kt-muted-fg);
|
|
|
|
|
margin-top: 0.4cqw;
|
|
|
|
|
margin-top: 0.4rem;
|
|
|
|
|
text-transform: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -635,16 +654,16 @@ const close = () => { emit('close') }
|
|
|
|
|
|
|
|
|
|
/* 底部 */
|
|
|
|
|
.kt-preview-footer {
|
|
|
|
|
padding: 1.5cqw;
|
|
|
|
|
padding: 1rem;
|
|
|
|
|
background: var(--kt-bg);
|
|
|
|
|
border-top: 2px solid var(--kt-border);
|
|
|
|
|
display: flex; justify-content: center; flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.kt-preview-thumb-list { display: flex; gap: 1cqw; overflow-x: auto; padding: 0.5cqw; }
|
|
|
|
|
.kt-preview-thumb-list { display: flex; gap: 0.5rem; overflow-x: auto; padding: 0.5rem; }
|
|
|
|
|
|
|
|
|
|
.kt-preview-thumb-item {
|
|
|
|
|
width: 4cqw; height: 4cqw;
|
|
|
|
|
width: 3rem; height: 3rem;
|
|
|
|
|
background: var(--kt-muted);
|
|
|
|
|
border: 2px solid var(--kt-border);
|
|
|
|
|
border-radius: var(--kt-radius);
|
|
|
|
|
@ -677,13 +696,13 @@ const close = () => { emit('close') }
|
|
|
|
|
color: var(--kt-fg);
|
|
|
|
|
border: 2px solid var(--kt-border);
|
|
|
|
|
border-radius: var(--kt-radius);
|
|
|
|
|
padding: 0.8cqh 1.6cqw;
|
|
|
|
|
padding: 0.5rem 1rem;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all var(--kt-transition-micro);
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 0.5cqw;
|
|
|
|
|
font-size: var(--cq-font-sm);
|
|
|
|
|
gap: 0.5rem;
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
}
|
|
|
|
|
@ -694,130 +713,29 @@ const close = () => { emit('close') }
|
|
|
|
|
border-color: var(--kt-accent);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
===========================================
|
|
|
|
|
深色模式适配 (Dark Mode)
|
|
|
|
|
===========================================
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
html.dark-mode .kt-preview-card {
|
|
|
|
|
background: var(--kt-bg);
|
|
|
|
|
border-color: var(--kt-border);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
html.dark-mode .kt-preview-header {
|
|
|
|
|
background: var(--kt-bg);
|
|
|
|
|
border-bottom-color: var(--kt-border);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
html.dark-mode .kt-preview-header-info h3 {
|
|
|
|
|
color: var(--kt-fg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
html.dark-mode .kt-preview-task-tag {
|
|
|
|
|
background: var(--kt-muted);
|
|
|
|
|
border-color: var(--kt-border);
|
|
|
|
|
color: var(--kt-muted-fg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
html.dark-mode .kt-preview-close-btn {
|
|
|
|
|
background: var(--kt-bg);
|
|
|
|
|
border-color: var(--kt-border);
|
|
|
|
|
color: var(--kt-muted-fg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
html.dark-mode .kt-preview-close-btn:hover {
|
|
|
|
|
background: var(--kt-accent);
|
|
|
|
|
color: var(--kt-accent-fg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
html.dark-mode .kt-preview-body {
|
|
|
|
|
background: var(--kt-bg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
html.dark-mode .kt-preview-loading,
|
|
|
|
|
html.dark-mode .kt-preview-error {
|
|
|
|
|
color: var(--kt-muted-fg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
html.dark-mode .kt-preview-img-box {
|
|
|
|
|
background: var(--kt-bg);
|
|
|
|
|
border-color: var(--kt-border);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
html.dark-mode .kt-preview-no-img {
|
|
|
|
|
color: var(--kt-muted-fg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
html.dark-mode .kt-preview-divider {
|
|
|
|
|
color: var(--kt-muted-fg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
html.dark-mode .kt-preview-nav-arrow {
|
|
|
|
|
background: var(--kt-bg);
|
|
|
|
|
border-color: var(--kt-border);
|
|
|
|
|
color: var(--kt-fg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
html.dark-mode .kt-preview-nav-arrow:hover {
|
|
|
|
|
background: var(--kt-accent);
|
|
|
|
|
border-color: var(--kt-accent);
|
|
|
|
|
color: var(--kt-accent-fg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
html.dark-mode .kt-preview-img-label {
|
|
|
|
|
background: var(--kt-muted);
|
|
|
|
|
border-color: var(--kt-border);
|
|
|
|
|
color: var(--kt-fg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
html.dark-mode .kt-preview-page-indicator {
|
|
|
|
|
background: var(--kt-muted);
|
|
|
|
|
border-color: var(--kt-border);
|
|
|
|
|
color: var(--kt-muted-fg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
html.dark-mode .kt-preview-report-stage {
|
|
|
|
|
background: var(--kt-bg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
html.dark-mode .kt-preview-report-container {
|
|
|
|
|
background: var(--kt-bg);
|
|
|
|
|
border-color: var(--kt-border);
|
|
|
|
|
scrollbar-color: var(--kt-border) var(--kt-bg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
html.dark-mode .kt-preview-report-container::-webkit-scrollbar-track {
|
|
|
|
|
background: var(--kt-bg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
html.dark-mode .kt-preview-report-container::-webkit-scrollbar-thumb {
|
|
|
|
|
background-color: var(--kt-border);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
html.dark-mode .kt-preview-report-header-tip {
|
|
|
|
|
background: var(--kt-muted);
|
|
|
|
|
border-color: var(--kt-border);
|
|
|
|
|
color: var(--kt-fg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
html.dark-mode .kt-preview-sub-tip {
|
|
|
|
|
color: var(--kt-muted-fg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
html.dark-mode .kt-preview-footer {
|
|
|
|
|
background: var(--kt-bg);
|
|
|
|
|
border-top-color: var(--kt-border);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
html.dark-mode .kt-preview-thumb-item {
|
|
|
|
|
background: var(--kt-muted);
|
|
|
|
|
border-color: var(--kt-border);
|
|
|
|
|
color: var(--kt-muted-fg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
html.dark-mode .kt-preview-thumb-item.active {
|
|
|
|
|
background: var(--kt-accent);
|
|
|
|
|
color: var(--kt-accent-fg);
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
/* Dark Mode */
|
|
|
|
|
html.dark-mode .kt-preview-card { background: var(--kt-bg); border-color: var(--kt-border); }
|
|
|
|
|
html.dark-mode .kt-preview-header { background: var(--kt-bg); border-bottom-color: var(--kt-border); }
|
|
|
|
|
html.dark-mode .kt-preview-header-info h3 { color: var(--kt-fg); }
|
|
|
|
|
html.dark-mode .kt-preview-task-tag { background: var(--kt-muted); border-color: var(--kt-border); color: var(--kt-muted-fg); }
|
|
|
|
|
html.dark-mode .kt-preview-close-btn { background: var(--kt-bg); border-color: var(--kt-border); color: var(--kt-muted-fg); }
|
|
|
|
|
html.dark-mode .kt-preview-close-btn:hover { background: var(--kt-accent); color: var(--kt-accent-fg); }
|
|
|
|
|
html.dark-mode .kt-preview-body { background: var(--kt-bg); }
|
|
|
|
|
html.dark-mode .kt-preview-loading, html.dark-mode .kt-preview-error { color: var(--kt-muted-fg); }
|
|
|
|
|
html.dark-mode .kt-preview-img-box { background: var(--kt-bg); border-color: var(--kt-border); }
|
|
|
|
|
html.dark-mode .kt-preview-no-img { color: var(--kt-muted-fg); }
|
|
|
|
|
html.dark-mode .kt-preview-divider { color: var(--kt-muted-fg); }
|
|
|
|
|
html.dark-mode .kt-preview-nav-arrow { background: var(--kt-bg); border-color: var(--kt-border); color: var(--kt-fg); }
|
|
|
|
|
html.dark-mode .kt-preview-nav-arrow:hover { background: var(--kt-accent); border-color: var(--kt-accent); color: var(--kt-accent-fg); }
|
|
|
|
|
html.dark-mode .kt-preview-img-label { background: var(--kt-muted); border-color: var(--kt-border); color: var(--kt-fg); }
|
|
|
|
|
html.dark-mode .kt-preview-page-indicator { background: var(--kt-muted); border-color: var(--kt-border); color: var(--kt-muted-fg); }
|
|
|
|
|
html.dark-mode .kt-preview-report-stage { background: var(--kt-bg); }
|
|
|
|
|
html.dark-mode .kt-preview-report-container { background: var(--kt-bg); border-color: var(--kt-border); scrollbar-color: var(--kt-border) var(--kt-bg); }
|
|
|
|
|
html.dark-mode .kt-preview-report-container::-webkit-scrollbar-track { background: var(--kt-bg); }
|
|
|
|
|
html.dark-mode .kt-preview-report-container::-webkit-scrollbar-thumb { background-color: var(--kt-border); }
|
|
|
|
|
html.dark-mode .kt-preview-report-header-tip { background: var(--kt-muted); border-color: var(--kt-border); color: var(--kt-fg); }
|
|
|
|
|
html.dark-mode .kt-preview-sub-tip { color: var(--kt-muted-fg); }
|
|
|
|
|
html.dark-mode .kt-preview-footer { background: var(--kt-bg); border-top-color: var(--kt-border); }
|
|
|
|
|
html.dark-mode .kt-preview-thumb-item { background: var(--kt-muted); border-color: var(--kt-border); color: var(--kt-muted-fg); }
|
|
|
|
|
html.dark-mode .kt-preview-thumb-item.active { background: var(--kt-accent); color: var(--kt-accent-fg); }
|
|
|
|
|
</style>
|