Files
LinkDesk/frontend/src/components/asset/AssetReferences.vue
T
indigo 26807984ee Phase 3: Unify controls across shot/asset/task toolbars and detail panels
Extracts duplication that built up as shot/asset/task features reached
parity: CheckableCommandItem/ColumnToggleList replace 14+ hand-rolled
checkbox-list blocks, shared toolbar pieces (debounced search, detail-panel
toggle, clear-filters, segmented view/context toggle) replace copy-pasted
markup in the three table toolbars, icon-only buttons standardize on the
icon-sm size, TaskBulkActionsMenu's Assign To submenu matches Set Status,
and a shared DetailPanelOverlay/Header/Loading/Error shell backs all three
detail panels (adding a previously-missing error state to the task panel).
2026-07-18 02:15:55 +08:00

120 lines
3.9 KiB
Vue

<template>
<div class="flex flex-col h-full">
<!-- References List -->
<div class="flex-1 overflow-y-auto p-4">
<div v-if="references.length === 0" class="flex flex-col items-center justify-center h-full text-muted-foreground">
<Image class="h-10 w-10 mb-2 opacity-50" />
<p class="text-sm">No reference files yet for this asset's tasks.</p>
</div>
<div v-else class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div
v-for="reference in references"
:key="reference.id"
class="border rounded-lg overflow-hidden hover:shadow-md transition-shadow"
>
<!-- File Preview -->
<div class="aspect-video bg-muted flex items-center justify-center relative">
<img
v-if="isImage(reference.file_path)"
:src="getFileUrl(reference.file_path)"
:alt="reference.file_name"
class="w-full h-full object-cover"
/>
<div v-else class="flex flex-col items-center gap-2 text-muted-foreground">
<FileIcon class="h-12 w-12" />
<span class="text-xs">{{ getFileExtension(reference.file_name) }}</span>
</div>
</div>
<!-- File Info -->
<div class="p-3 space-y-2">
<div class="flex items-start justify-between gap-2">
<div class="flex-1 min-w-0">
<p class="text-sm font-medium truncate">{{ reference.file_name }}</p>
<div class="flex items-center gap-2 mt-1">
<Badge variant="outline" class="text-xs">
{{ formatTaskType(reference.task_type) }}
</Badge>
</div>
<p class="text-xs text-muted-foreground mt-1">
{{ reference.task_name }}
</p>
</div>
<Button
variant="ghost"
size="icon-sm"
class="flex-shrink-0"
@click="downloadFile(reference)"
>
<Download class="h-4 w-4" />
</Button>
</div>
<p class="text-xs text-muted-foreground">
Uploaded {{ formatDate(reference.created_at) }}
</p>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { Image, FileIcon, Download } from 'lucide-vue-next'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
const props = defineProps<{
assetId: number
references: any[]
}>()
const emit = defineEmits<{
referencesUpdated: []
}>()
function isImage(filePath: string): boolean {
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg']
return imageExtensions.some(ext => filePath.toLowerCase().endsWith(ext))
}
function getFileUrl(filePath: string): string {
if (!filePath) return ''
if (filePath.startsWith('http')) return filePath
const cleanPath = filePath.replace(/^backend[\/\\]/, '').replace(/\\/g, '/')
return `http://localhost:8000/${cleanPath}`
}
function getFileExtension(fileName: string): string {
const parts = fileName.split('.')
return parts.length > 1 ? parts[parts.length - 1].toUpperCase() : 'FILE'
}
function formatTaskType(taskType: string): string {
return taskType.split('_').map(word =>
word.charAt(0).toUpperCase() + word.slice(1)
).join(' ')
}
function formatDate(dateString: string): string {
const date = new Date(dateString)
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
})
}
function downloadFile(reference: any) {
const url = getFileUrl(reference.file_path)
const link = document.createElement('a')
link.href = url
link.download = reference.file_name
link.target = '_blank'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
</script>