const state = { recordings: [], filtered: [], selectedKey: '', editing: null }; const elements = { refreshBtn: document.getElementById('refreshBtn'), searchInput: document.getElementById('searchInput'), typeFilter: document.getElementById('typeFilter'), clearSearchBtn: document.getElementById('clearSearchBtn'), uploadForm: document.getElementById('uploadForm'), uploadBtn: document.getElementById('uploadBtn'), recordingFile: document.getElementById('recordingFile'), fileNameText: document.getElementById('fileNameText'), uploadMeetingId: document.getElementById('uploadMeetingId'), uploadUserId: document.getElementById('uploadUserId'), totalCount: document.getElementById('totalCount'), meetingCount: document.getElementById('meetingCount'), storageSize: document.getElementById('storageSize'), recordingRootText: document.getElementById('recordingRootText'), listSummary: document.getElementById('listSummary'), loadingState: document.getElementById('loadingState'), emptyState: document.getElementById('emptyState'), recordingsTableWrap: document.getElementById('recordingsTableWrap'), recordingsTableBody: document.getElementById('recordingsTableBody'), previewVideo: document.getElementById('previewVideo'), previewPlaceholder: document.getElementById('previewPlaceholder'), previewTitle: document.getElementById('previewTitle'), previewDetails: document.getElementById('previewDetails'), editDialog: document.getElementById('editDialog'), editForm: document.getElementById('editForm'), editFilenameText: document.getElementById('editFilenameText'), editMeetingId: document.getElementById('editMeetingId'), editOriginalFilename: document.getElementById('editOriginalFilename'), editUserId: document.getElementById('editUserId'), closeEditBtn: document.getElementById('closeEditBtn'), cancelEditBtn: document.getElementById('cancelEditBtn'), notification: document.getElementById('notification'), notificationText: document.getElementById('notificationText') }; function recordingKey(recording) { return `${recording.meetingId}/${recording.filename}`; } function escapeHtml(value) { return String(value || '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function formatBytes(bytes) { const value = Number(bytes) || 0; if (value < 1024) { return `${value} B`; } const units = ['KB', 'MB', 'GB', 'TB']; let size = value / 1024; let index = 0; while (size >= 1024 && index < units.length - 1) { size /= 1024; index += 1; } return `${size.toFixed(size >= 10 ? 1 : 2)} ${units[index]}`; } function formatDate(value) { if (!value) { return '-'; } const date = new Date(value); if (Number.isNaN(date.getTime())) { return '-'; } return date.toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); } function showNotification(message, isError = false) { elements.notificationText.textContent = message; elements.notification.classList.toggle('recordings-notification-error', isError); elements.notification.classList.add('recordings-notification-visible'); window.clearTimeout(showNotification.timer); showNotification.timer = window.setTimeout(() => { elements.notification.classList.remove('recordings-notification-visible'); }, 2600); } async function requestJson(url, options = {}) { const response = await fetch(url, options); const payload = await response.json().catch(() => ({})); if (!response.ok || payload.success === false) { throw new Error(payload.message || `请求失败: ${response.status}`); } return payload; } function setLoading(isLoading) { elements.loadingState.classList.toggle('hidden', !isLoading); elements.recordingsTableWrap.classList.toggle('hidden', isLoading || state.filtered.length === 0); elements.emptyState.classList.toggle('hidden', isLoading || state.filtered.length > 0); } async function loadRecordings() { setLoading(true); try { const payload = await requestJson('/api/recordings'); state.recordings = payload.recordings || []; elements.recordingRootText.textContent = payload.root || 'recordings'; applyFilters(); const selected = state.filtered.find(item => recordingKey(item) === state.selectedKey) || state.filtered[0]; selectRecording(selected || null); } catch (error) { state.recordings = []; applyFilters(); selectRecording(null); showNotification(error.message, true); } finally { setLoading(false); } } function applyFilters() { const query = elements.searchInput.value.trim().toLowerCase(); const type = elements.typeFilter.value; state.filtered = state.recordings.filter((recording) => { const extension = (recording.filename || '').split('.').pop().toLowerCase(); const haystack = [ recording.meetingId, recording.filename, recording.originalFilename, recording.userId ].join(' ').toLowerCase(); return (type === 'all' || extension === type) && (!query || haystack.includes(query)); }); elements.clearSearchBtn.classList.toggle('hidden', !query && type === 'all'); renderSummary(); renderTable(); setLoading(false); } function renderSummary() { const meetings = new Set(state.recordings.map(recording => recording.meetingId)); const totalSize = state.recordings.reduce((sum, recording) => sum + (Number(recording.size) || 0), 0); elements.totalCount.textContent = state.recordings.length; elements.meetingCount.textContent = meetings.size; elements.storageSize.textContent = formatBytes(totalSize); elements.listSummary.textContent = `当前显示 ${state.filtered.length} 条,共 ${state.recordings.length} 条`; } function renderTable() { elements.recordingsTableBody.innerHTML = state.filtered.map((recording) => { const key = recordingKey(recording); const active = key === state.selectedKey ? 'recordings-row-active' : ''; const ext = escapeHtml((recording.filename || '').split('.').pop().toUpperCase()); return `
从左侧列表选择文件后,可播放、下载、编辑或删除。
'; renderTable(); return; } state.selectedKey = recordingKey(recording); elements.previewVideo.src = recording.streamUrl; elements.previewPlaceholder.classList.add('hidden'); elements.previewTitle.textContent = recording.originalFilename || recording.filename; elements.previewDetails.innerHTML = `