feat: add clickable file name bar with full-file playback

- Add card-file-info bar at top of search result cards
- Click file name to play full file (startFrame: 0, endFrame: 0)
- Show video/document icon based on file extension
- Display file UUID badge
- Remove duplicate card-file section from bottom of card
This commit is contained in:
2026-10-20 22:01:53 +08:00
parent c425c29d01
commit 971cfe50f5

View File

@@ -119,7 +119,14 @@
<div v-if="msg.results.length" class="card-grid">
<div v-for="(r, j) in msg.results" :key="j" class="result-card" :class="{ 'ocr-grouped': r.ocr_frames && r.ocr_frames.length > 1 }">
<div class="card-thumb" @click="playVideo(r)">
<!-- File info bar at top-left -->
<div class="card-file-info" @click.stop="playFullFile(r)">
<svg v-if="isVideoFile(r)" class="card-file-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"/></svg>
<svg v-else class="card-file-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
<span class="card-file-name">{{ r.file_name || 'Unknown' }}</span>
<span class="card-file-uuid-badge">{{ r.file_uuid }}</span>
</div>
<div class="card-thumb" @click="playVideo(r)">
<img v-if="r.thumbnail_url" :src="r.thumbnail_url" alt="" loading="lazy">
<img v-else-if="thumbs[r.file_uuid + ':' + r.start_frame]" :src="thumbs[r.file_uuid + ':' + r.start_frame]" alt="" loading="lazy">
<div v-else class="thumb-placeholder" v-observe="() => loadThumb(r.file_uuid, r.start_frame || 0)">
@@ -149,8 +156,6 @@
</div>
<div v-if="r._isPerson" class="card-text card-person-stats">{{ r.text_content }}</div>
<div v-if="r.text_content && !r._isPerson" class="card-text" v-html="highlightMatch(r.text_content.slice(0, 120), r.search_query)"></div>
<div class="card-file" v-if="!r._isPerson">{{ r.file_name || '' }}<br><span class="card-file-uuid">{{ r.file_uuid }}</span></div>
<div class="card-file card-person-file" v-if="r._isPerson">{{ r.display_file_name }}<br><span class="card-file-uuid">{{ r.file_uuid }}</span></div>
<div class="card-meta">
<span v-if="!r.ocr_frames && r.start_frame" class="card-frame">F{{ r.start_frame }}–{{ r.end_frame }}</span>
<span v-if="r.start_time != null" class="card-time">({{ formatTime(r.start_time) }}–{{ formatTime(r.end_time) }})</span>
@@ -1517,6 +1522,22 @@ function playVideoAtFrame(r: any, frame: number) {
playing.value = true
}
function playFullFile(r: any) {
currentVideo.value = {
fileUuid: r.file_uuid,
startFrame: 0,
endFrame: 0, // 0 = play to end
title: r.file_name || 'Full Video'
}
playing.value = true
}
function isVideoFile(r: any): boolean {
if (!r.file_name) return false
const ext = r.file_name.split('.').pop()?.toLowerCase()
return ['mp4', 'mov', 'mkv', 'avi', 'webm'].includes(ext || '')
}
function isVariantFrame(r: any, frame: number): boolean {
return r.variants?.some((v: any) => v.frames.includes(frame))
}
@@ -2068,6 +2089,26 @@ function openSource(src: SourceInfo) {
position: relative;
}
.card-thumb img { width: 100%; height: 100%; object-fit: cover; }
.card-file-info {
display: flex; align-items: center; gap: 5px;
padding: 5px 8px; background: var(--card-background);
border-bottom: 1px solid var(--border-light);
cursor: pointer; transition: background .15s;
}
.card-file-info:hover {
background: rgba(var(--primary-color-rgb), 0.1);
}
.card-file-icon {
flex-shrink: 0; color: var(--primary-color);
}
.card-file-name {
font-size: 11px; font-weight: 600; color: var(--text-primary);
flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.card-file-uuid-badge {
font-size: 8px; color: var(--muted-text); font-family: monospace;
max-width: 80px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ocr-group-badge {
position: absolute; top: 6px; right: 6px;
background: rgba(var(--primary-color-rgb), 0.85); color: #fff;