first implementation
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
<template>
|
||||
<div class="book-card card">
|
||||
<div class="book-header">
|
||||
<div class="book-info">
|
||||
<h3 class="book-title">{{ book.title }}</h3>
|
||||
<p class="book-filename">{{ book.fileName }}</p>
|
||||
</div>
|
||||
<span class="status-badge" :class="statusClass">{{ book.status }}</span>
|
||||
</div>
|
||||
|
||||
<div class="book-meta">
|
||||
<span v-if="book.fileSizeBytes" class="meta-item">
|
||||
{{ formatFileSize(book.fileSizeBytes) }}
|
||||
</span>
|
||||
<span v-if="book.pageCount" class="meta-item">
|
||||
{{ book.pageCount }} pages
|
||||
</span>
|
||||
<span class="meta-item">
|
||||
Uploaded {{ formatDate(book.uploadedAt) }}
|
||||
</span>
|
||||
<span v-if="book.processedAt" class="meta-item">
|
||||
Processed {{ formatDate(book.processedAt) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="book.status === 'FAILED' && book.errorMessage" class="error-message">
|
||||
<strong>Error:</strong> {{ book.errorMessage }}
|
||||
</div>
|
||||
|
||||
<div v-if="book.status === 'PENDING' || book.status === 'PROCESSING'" class="processing-indicator">
|
||||
<div class="spinner spinner-dark"></div>
|
||||
<span>{{ book.status === 'PENDING' ? 'Queued for processing...' : 'Embedding in progress...' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="book-actions">
|
||||
<button
|
||||
class="btn btn-danger"
|
||||
:disabled="book.status === 'PROCESSING' || deleting"
|
||||
@click="$emit('delete', book.id)"
|
||||
:title="book.status === 'PROCESSING' ? 'Cannot delete while processing' : 'Delete book'"
|
||||
>
|
||||
{{ deleting ? 'Deleting...' : 'Delete' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { Book } from '@/stores/bookStore'
|
||||
|
||||
const props = defineProps<{
|
||||
book: Book
|
||||
deleting?: boolean
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
(e: 'delete', id: string): void
|
||||
}>()
|
||||
|
||||
const statusClass = computed(() => {
|
||||
switch (props.book.status) {
|
||||
case 'READY':
|
||||
return 'status-ready'
|
||||
case 'PROCESSING':
|
||||
return 'status-processing'
|
||||
case 'PENDING':
|
||||
return 'status-pending'
|
||||
case 'FAILED':
|
||||
return 'status-failed'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
})
|
||||
|
||||
function formatFileSize(bytes: number): string {
|
||||
if (bytes < 1024) return `${bytes} B`
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
||||
}
|
||||
|
||||
function formatDate(iso: string): string {
|
||||
return new Date(iso).toLocaleString()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.book-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.book-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.book-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.book-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #1a365d;
|
||||
word-break: break-word;
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
.book-filename {
|
||||
font-size: 0.8rem;
|
||||
color: #718096;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
flex-shrink: 0;
|
||||
padding: 0.25rem 0.65rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.status-ready {
|
||||
background: #c6f6d5;
|
||||
color: #22543d;
|
||||
}
|
||||
|
||||
.status-processing {
|
||||
background: #bee3f8;
|
||||
color: #1a365d;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background: #fefcbf;
|
||||
color: #744210;
|
||||
}
|
||||
|
||||
.status-failed {
|
||||
background: #fed7d7;
|
||||
color: #742a2a;
|
||||
}
|
||||
|
||||
.book-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
font-size: 0.8rem;
|
||||
color: #718096;
|
||||
background: #f7fafc;
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
font-size: 0.85rem;
|
||||
color: #742a2a;
|
||||
background: #fff5f5;
|
||||
border: 1px solid #fed7d7;
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
.processing-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
color: #2b6cb0;
|
||||
}
|
||||
|
||||
.book-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user