diff --git a/backend/app/schemas.py b/backend/app/schemas.py index c06e017..5d616b3 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -57,6 +57,7 @@ class VideoInfo(BaseModel): file_size: int duration: int status: str + error_message: str = "" created_at: datetime class Config: diff --git a/frontend/src/views/Admin.vue b/frontend/src/views/Admin.vue index fefa0c9..e9a2ae6 100644 --- a/frontend/src/views/Admin.vue +++ b/frontend/src/views/Admin.vue @@ -24,16 +24,32 @@
- + +
+ +
+ {{ platformIcon(v.platform || v.url) }} +
+ +
+
🕐
+
+
-

{{ v.title || 'Untitled' }}

+

{{ v.title || truncateUrl(v.url) }}

- {{ v.status }} - {{ v.platform }} - {{ v.quality }} - {{ humanSize(v.file_size) }} + {{ statusLabel(v.status) }} + {{ v.platform }} + {{ v.quality }} + {{ humanSize(v.file_size) }} {{ fmtTime(v.created_at) }}
+ +
+ ⚠️ {{ v.error_message }} +
+ +
{{ v.url }}
@@ -310,6 +326,23 @@ function fmtTime(ts) { return new Date(ts).toLocaleString('zh-CN', { hour12: false }) } +function platformIcon(platformOrUrl) { + const s = (platformOrUrl || '').toLowerCase() + if (s.includes('youtube')) return '▶' + if (s.includes('pornhub')) return '🔞' + if (s.includes('twitter') || s.includes('x.com')) return '𝕏' + return '🎬' +} + +function statusLabel(status) { + return { done: '✅ Done', downloading: '⏳ Downloading', pending: '🕐 Pending', error: '❌ Error', deleted: '🗑 Deleted' }[status] || status +} + +function truncateUrl(url) { + if (!url) return 'Unknown' + try { return new URL(url).hostname + '...' } catch { return url.slice(0, 40) + '...' } +} + function browserIcon(b) { return { Chrome: '🌐', Firefox: '🦊', Safari: '🧭', Edge: '🔷', Opera: '🔴', Samsung: '📱' }[b] || '🌐' } @@ -428,14 +461,34 @@ onMounted(() => { fetchVideos(); fetchStats() }) border: 1px solid #2a2a3e; } .video-main { display: flex; gap: 1.2rem; flex: 1; min-width: 0; } -.thumb { width: 112px; height: 63px; object-fit: cover; border-radius: 6px; flex-shrink: 0; } +.thumb-wrap { position: relative; flex-shrink: 0; width: 112px; height: 63px; } +.thumb { width: 112px; height: 63px; object-fit: cover; border-radius: 6px; } +.thumb-placeholder { + width: 112px; height: 63px; border-radius: 6px; background: #12122a; + border: 1px solid #333; display: flex; align-items: center; justify-content: center; + font-size: 1.6rem; color: #555; +} +.thumb-overlay { + position: absolute; inset: 0; border-radius: 6px; + background: rgba(0,0,0,0.55); display: flex; align-items: center; justify-content: center; + font-size: 1.3rem; +} +.err-overlay { background: rgba(180,0,0,0.45); } .info { min-width: 0; } .info h4 { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-bottom: 0.4rem; font-size: 1rem; } .meta { display: flex; gap: 1rem; font-size: 0.88rem; color: #888; flex-wrap: wrap; } +.status-badge { font-size: 0.82rem; font-weight: 600; } .status-done { color: #27ae60; } .status-downloading { color: #f39c12; } .status-error { color: #e74c3c; } .status-pending { color: #888; } +.status-deleted { color: #555; } +.platform-tag { color: #1da1f2; font-size: 0.82rem; text-transform: capitalize; } +.error-msg { + color: #e74c3c; font-size: 0.8rem; margin-top: 0.3rem; + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 500px; +} +.video-url { color: #555; font-size: 0.78rem; margin-top: 0.2rem; word-break: break-all; } .actions { display: flex; gap: 0.4rem; flex-shrink: 0; } .actions button, .actions a { padding: 0.4rem 0.6rem; border: 1px solid #444; border-radius: 6px;