|
2 | 2 | const state = { |
3 | 3 | role: 'client', |
4 | 4 | items: [], |
| 5 | + downloadStats: {}, |
5 | 6 | apiBase: '', |
6 | 7 | events: null, |
| 8 | + downloadEvents: null, |
7 | 9 | isTauri: false, |
8 | 10 | serverRunning: false, |
9 | 11 | shareInfo: null, |
|
24 | 26 | return `${(size / 1024 / 1024).toFixed(1)} MB`; |
25 | 27 | } |
26 | 28 |
|
| 29 | + function formatSpeed(size) { |
| 30 | + if (!Number.isFinite(size) || size <= 0) return '0 B/s'; |
| 31 | + return `${formatSize(size)}/s`; |
| 32 | + } |
| 33 | + |
27 | 34 | function formatTime(value) { |
28 | 35 | return new Date(value).toLocaleString('zh-CN', { hour12: false }); |
29 | 36 | } |
|
64 | 71 |
|
65 | 72 | root.innerHTML = state.items.map((item) => { |
66 | 73 | const isText = item.kind === 'text'; |
| 74 | + const isMissing = item.kind !== 'text' && item.exists === false; |
67 | 75 | const badge = isText ? '文本' : '文件'; |
68 | 76 | const title = isText |
69 | 77 | ? escapeHtml(previewText(item.content || item.title, 40)) |
70 | 78 | : escapeHtml(item.title); |
71 | 79 | const titleAction = isText |
72 | 80 | ? '' |
73 | 81 | : `<button class="inline-icon-button" data-action="copy-link" data-id="${item.id}" aria-label="复制文件链接" title="复制文件链接">🔗</button>`; |
| 82 | + const downloadStat = state.role === 'admin' && !isText ? state.downloadStats[item.id] : null; |
| 83 | + const downloadStatus = downloadStat |
| 84 | + ? `<span class="download-status">⬇ ${formatSpeed(downloadStat.speedBps)}</span>` |
| 85 | + : ''; |
74 | 86 | const description = isText |
75 | 87 | ? `<div class="meta">${formatTextLength(item.content || item.title)} · ${formatTime(item.createdAt)}</div>` |
76 | | - : `<div class="meta">${formatSize(item.size)} · ${formatTime(item.createdAt)}</div>`; |
| 88 | + : `<div class="meta">${formatSize(item.size)} · ${formatTime(item.createdAt)}${downloadStatus}</div>`; |
77 | 89 | const primaryAction = isText |
78 | 90 | ? `<button class="secondary" data-action="copy" data-id="${item.id}">复制</button>` |
79 | | - : `<button class="secondary" data-action="download" data-id="${item.id}">下载</button>`; |
| 91 | + : (isMissing |
| 92 | + ? `<button class="secondary" disabled type="button">已失效</button>` |
| 93 | + : (state.role === 'admin' |
| 94 | + ? `<button class="secondary" data-action="reveal" data-id="${item.id}">查看</button>` |
| 95 | + : `<button class="secondary" data-action="download" data-id="${item.id}">下载</button>`)); |
80 | 96 | const actions = state.role === 'admin' |
81 | | - ? `${primaryAction}<button class="danger" data-action="delete" data-id="${item.id}">删除</button>` |
| 97 | + ? `${primaryAction}<button class="secondary remove-action" data-action="delete" data-id="${item.id}">移除</button>` |
82 | 98 | : primaryAction; |
83 | 99 | return ` |
84 | | - <article class="item"> |
| 100 | + <article class="item${isMissing ? ' item-missing' : ''}"> |
85 | 101 | <div class="item-main"> |
86 | 102 | <div class="item-title"> |
87 | 103 | <span class="badge">${badge}</span> |
|
189 | 205 | state.serverRunning = false; |
190 | 206 | state.apiBase = ''; |
191 | 207 | state.items = []; |
| 208 | + state.downloadStats = {}; |
192 | 209 | if (state.events) { |
193 | 210 | state.events.close(); |
194 | 211 | state.events = null; |
195 | 212 | } |
| 213 | + if (state.downloadEvents) { |
| 214 | + state.downloadEvents.close(); |
| 215 | + state.downloadEvents = null; |
| 216 | + } |
196 | 217 | const root = $('items'); |
197 | 218 | if (root) root.innerHTML = ''; |
198 | 219 | document.body.classList.add('server-stopped'); |
|
311 | 332 | state.items = JSON.parse(event.data); |
312 | 333 | render(); |
313 | 334 | }; |
| 335 | + if (state.role === 'admin') { |
| 336 | + connectDownloadEvents(); |
| 337 | + } |
| 338 | + } |
| 339 | + |
| 340 | + function connectDownloadEvents() { |
| 341 | + if (state.downloadEvents) state.downloadEvents.close(); |
| 342 | + const events = new EventSource(`${state.apiBase}/api/download-events`); |
| 343 | + state.downloadEvents = events; |
| 344 | + events.onerror = () => { |
| 345 | + state.downloadStats = {}; |
| 346 | + render(); |
| 347 | + }; |
| 348 | + events.onmessage = (event) => { |
| 349 | + const stats = JSON.parse(event.data); |
| 350 | + state.downloadStats = Object.fromEntries( |
| 351 | + stats |
| 352 | + .filter((item) => item.activeCount > 0) |
| 353 | + .map((item) => [item.itemId, item]) |
| 354 | + ); |
| 355 | + render(); |
| 356 | + }; |
314 | 357 | } |
315 | 358 |
|
316 | 359 | async function addAdminLocalFiles(paths) { |
|
420 | 463 | }, 1600); |
421 | 464 | } |
422 | 465 |
|
| 466 | + function showStatusMessage(message) { |
| 467 | + const status = $('status'); |
| 468 | + if (status) { |
| 469 | + status.textContent = message; |
| 470 | + status.classList.add('status-error'); |
| 471 | + window.setTimeout(() => { |
| 472 | + status.classList.remove('status-error'); |
| 473 | + if (state.serverRunning) { |
| 474 | + status.textContent = '实时同步'; |
| 475 | + } |
| 476 | + }, 1800); |
| 477 | + } |
| 478 | + } |
| 479 | + |
423 | 480 | async function bindAdminFileDrop() { |
424 | 481 | if (state.role !== 'admin' || state.adminDragDropBound) return; |
425 | 482 |
|
|
664 | 721 | $('items').addEventListener('click', async (event) => { |
665 | 722 | const button = event.target.closest('button[data-action]'); |
666 | 723 | if (!button) return; |
| 724 | + if (button.disabled) return; |
667 | 725 | const { action, id } = button.dataset; |
| 726 | + if (action === 'reveal' && state.role === 'admin') { |
| 727 | + const item = state.items.find((entry) => entry.id === id); |
| 728 | + if (!item) return; |
| 729 | + if (state.isTauri) { |
| 730 | + try { |
| 731 | + await window.__TAURI__.core.invoke('reveal_admin_file', { id }); |
| 732 | + } catch (error) { |
| 733 | + const message = error?.message || String(error) || '无法打开文件位置'; |
| 734 | + showStatusMessage(`打开失败:${message}`); |
| 735 | + window.alert(`打开失败:${message}`); |
| 736 | + } |
| 737 | + return; |
| 738 | + } |
| 739 | + window.open(getDownloadUrl(item), '_blank'); |
| 740 | + } |
668 | 741 | if (action === 'download') { |
669 | | - if (state.role === 'admin' && state.isTauri) { |
670 | | - await window.__TAURI__.core.invoke('download_admin_file', { id }); |
| 742 | + const item = state.items.find((entry) => entry.id === id); |
| 743 | + if (!item) return; |
| 744 | + if (item.exists === false) { |
| 745 | + alert('源文件已不存在'); |
671 | 746 | return; |
672 | 747 | } |
673 | 748 | window.location.href = `${state.apiBase}/api/items/${id}/download`; |
|
0 commit comments